Compare commits

...
Author SHA1 Message Date
emrysal 2a0991c259 chore: release v5.6.1
Post release / release (push) Canceled after 0s
2025-08-18 23:55:48 +00:00
Eunjae LeeandDevin AI 2e454c9723 refactor: rename insights routing service files (#22917)
- Rename insightsRoutingDI.ts -> InsightsRoutingDIService.ts
- Rename insightsRoutingBase.ts -> InsightsRoutingBaseService.ts
- Update all import statements to use new file names
- No functional changes, only file renames and import updates

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-18 15:15:21 +00:00
Anik Dhabal Babu 7e6d94c8e0 fix: team admin/owner can't change others member role (#23161)
* fix: team admin/owner can't change member role

* update

* Update role-management.factory.ts

* address review
2025-08-18 14:57:22 +00:00
Volnei Munhoz e18b790ccd chore: Upgrade Turborepo from 1 to 2.5 (#23101)
* upgrade turborepo from 1 to 2.5

* Fix tsconfig.json paths path

* chore: fix root path on tsconfig.paths

* chore: add baseUrl to tsconfig.json

* chore: tsconfig.json issue

* try: CI without tsconfig paths

* chore: add only local paths on tsconfig.json

* chore: Fix docs

* Fix missing env var on turbo.json
2025-08-18 11:17:27 -03:00
Morgan 0274417ad1 fix: api v2 workflow controller trigger validation (#23159) 2025-08-18 16:15:40 +03:00
a8a3a93ace fix: redirect to existing payment session on paid booking retry (#23096)
* fix: redirect to existing payment page on booking retry

- Extract existing payment UID from unpaid bookings
- Return paymentUid and paymentId when payment form should be shown
- Prevents payment form bypass by redirecting to original payment session
- Maintains payment continuity without creating duplicate bookings

Fixes issue where canceled payments showed success page on rebooking

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* test: add unit test for payment retry scenario

- Test verifies existing payment UID is returned on booking retry
- Ensures no duplicate bookings are created in database
- Validates paymentRequired flag is set correctly for unpaid bookings
- Covers the payment redirect fix in handleNewBooking.ts

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* update

* fix: restore calendar event deletion for round robin reassignment

- Add deleteEventsAndMeetings call back to EventManager.reschedule() when changedOrganizer is true
- Add unit test to verify calendar deletion during round robin reassignment
- Fixes regression where calendar events weren't deleted from original host's calendar

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

* Update roundRobinReassignment.test.ts

* Update EventManager.ts

* test: update payment retry test title to single descriptive line

- Changed from numbered list format to concise single line description
- Test functionality remains unchanged and passes successfully

Co-Authored-By: anik@cal.com <adhabal2002@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-18 11:44:40 +00:00
88bbab216e fix: Consistently remove a member from organization/sub-team/team - through all the APIs and webapp actions (#22806)
* Fix membership deletion handling and add tests

* fixes

* revert api-v2 specific changes

* Add more tests

* refactor: Move removeMember function into TeamService as private static methods

- Moved removeMember and all related helper functions from separate file into TeamService class
- Made all functions private static methods instead of exporting them
- Deleted the original removeMember.ts file since it's no longer needed
- Updated imports to use the new location
- All integration tests pass successfully

* refactor: Rename memberId to userId in TeamService methods

- Renamed memberId parameter to userId in removeMember private method
- Renamed memberIds parameter to userIds in removeMembers public method
- Updated all calls to removeMembers to use userIds instead of memberIds
- Parameter names now accurately reflect that they are user IDs, not membership IDs

* test: add service unit tests and simplify e2e tests for membership deletion

- Add unit tests for membership deletion services
- Remove redundant deletion behavior tests from controllers
- Keep only happy path tests in e2e controller tests
- Fix unused imports and variables

* package.json version

* fix unit teswtes

* No specs file

* fix unit tests

* fix: Add platform-libraries build step to E2E API v2 workflow

The E2E API v2 tests were failing with TypeScript compilation errors because
TeamService imports from @calcom/platform-libraries require the package to be
built first to generate the dist/ folder with compiled exports.

This adds the same build step that was added to unit-tests.yml to resolve
the module resolution errors.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* Making tests clearer and easier to undestand

* chore: bump platform libs

* chore: bump platform libs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: cal.com <morgan@cal.com>
2025-08-18 08:38:32 -03:00
Chinmay Patil f74434a282 fix: show temporarily out-of-office message on all screen sizes (#23130) 2025-08-18 16:56:49 +05:30
Joe Au-Yeung ed6f9d8252 fix: Add cal.eu to IS_CALCOM (#23111)
* Add cal.eu to `IS_CALCOM`

* Create `getCalcomUrl` function

* Use `getCalcomURl` in generate metadata

* Use `getCalcomURl` in `buildCanonical`

* Linting fix
2025-08-18 16:04:48 +05:30
Benny Joo a19ccab16a perf: use promise.all in /apps/installed (#23146) 2025-08-18 10:11:22 +05:30
Benny Joo 77369a13f1 chore: remove unused orgMigrations files (#23056) 2025-08-17 22:18:14 -03:00
Anik Dhabal Babu 8231356d2a test: E2E test to verify event multiple duration works correctly (#23144)
* fix: multiple booking duration isn't working

* followup test
2025-08-17 20:26:14 +01:00
Anik Dhabal Babu 469a942b5a fix: multiple booking duration isn't working (#23140) 2025-08-17 17:08:27 +00:00
keithwillcode 0afe6cf5d1 chore: release v5.6.0
Post release / release (push) Canceled after 0s
2025-08-15 15:32:31 +00:00
56 changed files with 1928 additions and 836 deletions
+2 -2
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+2
View File
@@ -0,0 +1,2 @@
undecided:
- "@calcom/prisma"
+1 -1
View File
@@ -199,7 +199,7 @@ There's some settings that you'll need to setup.
Under Vercel > Your API Project > Settings Under Vercel > Your API Project > Settings
In General > Build & Development Settings In General > Build & Development Settings
BUILD COMMAND: `yarn turbo run build --scope=@calcom/api --include-dependencies --no-deps` BUILD COMMAND: `yarn turbo run build --filter=@calcom/api --no-deps`
OUTPUT DIRECTORY: `apps/api/.next` OUTPUT DIRECTORY: `apps/api/.next`
In Git > Ignored Build Step In Git > Ignored Build Step
+1 -1
View File
@@ -38,7 +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.312", "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.314",
"@calcom/platform-types": "*", "@calcom/platform-types": "*",
"@calcom/platform-utils": "*", "@calcom/platform-utils": "*",
"@calcom/prisma": "*", "@calcom/prisma": "*",
@@ -2,6 +2,8 @@ import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inpu
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository"; import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input"; import { UpdateOrgMembershipDto } from "../inputs/update-organization-membership.input";
import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service"; import { OrganizationsMembershipOutputService } from "./organizations-membership-output.service";
@@ -62,10 +64,24 @@ export class OrganizationsMembershipService {
} }
async deleteOrgMembership(organizationId: number, membershipId: number) { async deleteOrgMembership(organizationId: number, membershipId: number) {
const membership = await this.organizationsMembershipRepository.deleteOrgMembership( // Get the membership first to get the userId
const membership = await this.organizationsMembershipRepository.findOrgMembership(
organizationId, organizationId,
membershipId membershipId
); );
if (!membership) {
throw new NotFoundException(
`Membership with id ${membershipId} within organization id ${organizationId} not found`
);
}
await TeamService.removeMembers({
teamIds: [organizationId],
userIds: [membership.userId],
isOrg: true,
});
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership); return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
} }
@@ -20,7 +20,6 @@ import {
OrgTeamMembershipOutputResponseDto, OrgTeamMembershipOutputResponseDto,
} from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output"; } from "@/modules/organizations/teams/memberships/outputs/organization-teams-memberships.output";
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service"; import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/teams/memberships/services/organizations-teams-memberships.service";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output"; import { TeamMembershipOutput } from "@/modules/teams/memberships/outputs/team-membership.output";
import { import {
Controller, Controller,
@@ -59,7 +58,6 @@ export class OrganizationsTeamsMembershipsController {
constructor( constructor(
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService, private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
private teamsEventTypesService: TeamsEventTypesService,
private readonly organizationsRepository: OrganizationsRepository private readonly organizationsRepository: OrganizationsRepository
) {} ) {}
@@ -127,8 +125,6 @@ export class OrganizationsTeamsMembershipsController {
membershipId membershipId
); );
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return { return {
status: SUCCESS_STATUS, status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
@@ -3,6 +3,8 @@ import { UpdateOrgTeamMembershipDto } from "@/modules/organizations/teams/member
import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository"; import { OrganizationsTeamsMembershipsRepository } from "@/modules/organizations/teams/memberships/organizations-teams-memberships.repository";
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
@Injectable() @Injectable()
export class OrganizationsTeamsMembershipsService { export class OrganizationsTeamsMembershipsService {
constructor( constructor(
@@ -58,11 +60,21 @@ export class OrganizationsTeamsMembershipsService {
} }
async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) { async deleteOrgTeamMembership(organizationId: number, teamId: number, membershipId: number) {
const teamMembership = await this.organizationsTeamsMembershipsRepository.deleteOrgTeamMembershipById( // First get the membership to get the userId
const teamMembership = await this.organizationsTeamsMembershipsRepository.findOrgTeamMembership(
organizationId, organizationId,
teamId, teamId,
membershipId membershipId
); );
if (!teamMembership) {
throw new NotFoundException(
`Membership with id ${membershipId} not found in team ${teamId} of organization ${organizationId}`
);
}
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
return teamMembership; return teamMembership;
} }
} }
@@ -12,7 +12,12 @@ import {
EMAIL, EMAIL,
WorkflowEmailAttendeeStepDto, WorkflowEmailAttendeeStepDto,
} from "@/modules/workflows/inputs/workflow-step.input"; } from "@/modules/workflows/inputs/workflow-step.input";
import { BEFORE_EVENT, DAY } from "@/modules/workflows/inputs/workflow-trigger.input"; import {
BEFORE_EVENT,
DAY,
OnAfterEventTriggerDto,
OnBeforeEventTriggerDto,
} from "@/modules/workflows/inputs/workflow-trigger.input";
// Adjust path if needed // Adjust path if needed
import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output"; import { GetWorkflowOutput, GetWorkflowsOutput } from "@/modules/workflows/outputs/workflow.output";
import { INestApplication } from "@nestjs/common"; import { INestApplication } from "@nestjs/common";
@@ -255,6 +260,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
"+37255555555" "+37255555555"
); );
expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail); expect(responseBody.data.steps.find((step) => step.stepNumber === 3)?.email).toEqual(authEmail);
const trigger = sampleCreateWorkflowDto.trigger as OnBeforeEventTriggerDto;
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
createdWorkflowId = responseBody.data.id; createdWorkflowId = responseBody.data.id;
createdWorkflow = responseBody.data; createdWorkflow = responseBody.data;
}); });
@@ -330,6 +339,13 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
expect(step1).toBeDefined(); expect(step1).toBeDefined();
const partialUpdateDto: Partial<CreateWorkflowDto> = { const partialUpdateDto: Partial<CreateWorkflowDto> = {
name: updatedName, name: updatedName,
trigger: {
type: "afterEvent",
offset: {
unit: "minute",
value: 10,
},
},
steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [], steps: step1 ? [{ ...step1, sender: "updatedSender" } as WorkflowEmailAttendeeStepDto] : [],
}; };
expect(createdWorkflowId).toBeDefined(); expect(createdWorkflowId).toBeDefined();
@@ -348,6 +364,10 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id); step1 && expect(responseBody.data.steps[0].id).toEqual(step1.id);
expect(responseBody.data.steps[0].sender).toEqual("updatedSender"); expect(responseBody.data.steps[0].sender).toEqual("updatedSender");
expect(responseBody.data.steps[1]?.id).toBeUndefined(); expect(responseBody.data.steps[1]?.id).toBeUndefined();
const trigger = partialUpdateDto.trigger as OnAfterEventTriggerDto;
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
expect(responseBody.data.trigger?.offset?.value).toEqual(trigger.offset.value);
expect(responseBody.data.trigger?.offset?.unit).toEqual(trigger.offset.unit);
}); });
}); });
@@ -156,17 +156,4 @@ export class TeamsEventTypesService {
return this.eventTypesRepository.deleteEventType(eventTypeId); return this.eventTypesRepository.deleteEventType(eventTypeId);
} }
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
try {
await this.teamsEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
await this.teamsEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
} catch (err) {
this.logger.error("Could not remove user from all team event-types.", {
error: err,
userId,
teamId,
});
}
}
} }
@@ -3,7 +3,6 @@ import { API_KEY_HEADER } from "@/lib/docs/headers";
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input"; import { CreateTeamMembershipInput } from "@/modules/teams/memberships/inputs/create-team-membership.input";
import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input"; import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/update-team-membership.input";
import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output"; import { CreateTeamMembershipOutput } from "@/modules/teams/memberships/outputs/create-team-membership.output";
@@ -45,10 +44,7 @@ import { SkipTakePagination } from "@calcom/platform-types";
export class TeamsMembershipsController { export class TeamsMembershipsController {
private logger = new Logger("TeamsMembershipsController"); private logger = new Logger("TeamsMembershipsController");
constructor( constructor(private teamsMembershipsService: TeamsMembershipsService) {}
private teamsMembershipsService: TeamsMembershipsService,
private teamsEventTypesService: TeamsEventTypesService
) {}
@Roles("TEAM_ADMIN") @Roles("TEAM_ADMIN")
@Post("/") @Post("/")
@@ -127,7 +123,6 @@ export class TeamsMembershipsController {
membershipId, membershipId,
body body
); );
if (!currentMembership.accepted && updatedMembership.accepted) { if (!currentMembership.accepted && updatedMembership.accepted) {
try { try {
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId); await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
@@ -151,8 +146,6 @@ export class TeamsMembershipsController {
): Promise<DeleteTeamMembershipOutput> { ): Promise<DeleteTeamMembershipOutput> {
const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId); const membership = await this.teamsMembershipsService.deleteTeamMembership(teamId, membershipId);
await this.teamsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
return { return {
status: SUCCESS_STATUS, status: SUCCESS_STATUS,
data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }), data: plainToClass(TeamMembershipOutput, membership, { strategy: "excludeAll" }),
@@ -3,6 +3,8 @@ import { UpdateTeamMembershipInput } from "@/modules/teams/memberships/inputs/up
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository"; import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
import { Injectable, NotFoundException } from "@nestjs/common"; import { Injectable, NotFoundException } from "@nestjs/common";
import { TeamService } from "@calcom/platform-libraries";
@Injectable() @Injectable()
export class TeamsMembershipsService { export class TeamsMembershipsService {
constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {} constructor(private readonly teamsMembershipsRepository: TeamsMembershipsRepository) {}
@@ -41,10 +43,15 @@ export class TeamsMembershipsService {
} }
async deleteTeamMembership(teamId: number, membershipId: number) { async deleteTeamMembership(teamId: number, membershipId: number) {
const teamMembership = await this.teamsMembershipsRepository.deleteTeamMembershipById( // First get the membership to get the userId
teamId, const teamMembership = await this.teamsMembershipsRepository.findTeamMembership(teamId, membershipId);
membershipId
); if (!teamMembership) {
throw new NotFoundException(`Membership with id ${membershipId} not found in team ${teamId}`);
}
await TeamService.removeMembers({ teamIds: [teamId], userIds: [teamMembership.userId], isOrg: false });
return teamMembership; return teamMembership;
} }
} }
@@ -1,7 +1,7 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client"; import { TimeUnit, WorkflowTriggerEvents } from "@prisma/client";
import { Type } from "class-transformer"; import { Type } from "class-transformer";
import { IsNumber, ValidateNested } from "class-validator"; import { IsIn, IsNumber, IsString, ValidateNested } from "class-validator";
export const BEFORE_EVENT = "beforeEvent"; export const BEFORE_EVENT = "beforeEvent";
export const EVENT_CANCELLED = "eventCancelled"; export const EVENT_CANCELLED = "eventCancelled";
@@ -68,6 +68,8 @@ export class WorkflowTriggerOffsetDto {
value!: number; value!: number;
@ApiProperty({ description: "Unit for the offset time", example: HOUR }) @ApiProperty({ description: "Unit for the offset time", example: HOUR })
@IsString()
@IsIn(TIME_UNITS)
unit!: TimeUnitType; unit!: TimeUnitType;
} }
@@ -75,6 +77,8 @@ export class BaseWorkflowTriggerDto {
@ApiProperty({ @ApiProperty({
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
}) })
@IsString()
@IsIn([WORKFLOW_TRIGGER_TYPES])
type!: WorkflowTriggerType; type!: WorkflowTriggerType;
} }
@@ -82,6 +86,8 @@ export class OnCreationTriggerDto {
@ApiProperty({ @ApiProperty({
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
}) })
@IsString()
@IsIn([NEW_EVENT])
type: typeof NEW_EVENT = NEW_EVENT; type: typeof NEW_EVENT = NEW_EVENT;
} }
@@ -89,12 +95,16 @@ export class OnRescheduleTriggerDto {
@ApiProperty({ @ApiProperty({
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
}) })
@IsString()
@IsIn([RESCHEDULE_EVENT])
type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT; type: typeof RESCHEDULE_EVENT = RESCHEDULE_EVENT;
} }
export class OnCancelTriggerDto { export class OnCancelTriggerDto {
@ApiProperty({ @ApiProperty({
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
}) })
@IsString()
@IsIn([EVENT_CANCELLED])
type: typeof EVENT_CANCELLED = EVENT_CANCELLED; type: typeof EVENT_CANCELLED = EVENT_CANCELLED;
} }
@@ -113,6 +123,8 @@ export class OnBeforeEventTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
example: BEFORE_EVENT, example: BEFORE_EVENT,
}) })
@IsString()
@IsIn([BEFORE_EVENT])
type: typeof BEFORE_EVENT = BEFORE_EVENT; type: typeof BEFORE_EVENT = BEFORE_EVENT;
} }
@@ -121,6 +133,8 @@ export class OnAfterEventTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
example: AFTER_EVENT, example: AFTER_EVENT,
}) })
@IsString()
@IsIn([AFTER_EVENT])
type: typeof AFTER_EVENT = AFTER_EVENT; type: typeof AFTER_EVENT = AFTER_EVENT;
} }
@@ -129,6 +143,8 @@ export class OnAfterCalVideoGuestsNoShowTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW, example: AFTER_GUESTS_CAL_VIDEO_NO_SHOW,
}) })
@IsString()
@IsIn([AFTER_GUESTS_CAL_VIDEO_NO_SHOW])
type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW; type: typeof AFTER_GUESTS_CAL_VIDEO_NO_SHOW = AFTER_GUESTS_CAL_VIDEO_NO_SHOW;
} }
@@ -137,5 +153,7 @@ export class OnAfterCalVideoHostsNoShowTriggerDto extends TriggerOffsetDTO {
description: "Trigger type for the workflow", description: "Trigger type for the workflow",
example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW, example: AFTER_HOSTS_CAL_VIDEO_NO_SHOW,
}) })
@IsString()
@IsIn([AFTER_HOSTS_CAL_VIDEO_NO_SHOW])
type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW; type: typeof AFTER_HOSTS_CAL_VIDEO_NO_SHOW = AFTER_HOSTS_CAL_VIDEO_NO_SHOW;
} }
@@ -59,4 +59,8 @@ export class EventTypesRepositoryFixture {
async delete(eventTypeId: EventType["id"]) { async delete(eventTypeId: EventType["id"]) {
return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } }); return this.prismaWriteClient.eventType.delete({ where: { id: eventTypeId } });
} }
async findById(eventTypeId: EventType["id"]) {
return this.prismaReadClient.eventType.findUnique({ where: { id: eventTypeId } });
}
} }
@@ -35,4 +35,8 @@ export class MembershipRepositoryFixture {
await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } }); await this.prismaWriteClient.user.update({ where: { id: user.id }, data: { organizationId: org.id } });
return membership; return membership;
} }
async findById(membershipId: Membership["id"]) {
return this.prismaReadClient.membership.findUnique({ where: { id: membershipId } });
}
} }
@@ -45,11 +45,13 @@ const InstalledAppsWrapper = async ({ params }: PageProps) => {
createRouterCaller(appsRouter), createRouterCaller(appsRouter),
]); ]);
const connectedCalendars = await calendarsCaller.connectedCalendars(); const [connectedCalendars, installedCalendars] = await Promise.all([
const installedCalendars = await appsCaller.integrations({ calendarsCaller.connectedCalendars(),
variant: "calendar", appsCaller.integrations({
onlyInstalled: true, variant: "calendar",
}); onlyInstalled: true,
}),
]);
return ( return (
<InstalledApps <InstalledApps
@@ -1,4 +1,3 @@
import { _generateMetadata } from "app/_utils";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { FeaturesRepository } from "@calcom/features/flags/features.repository";
+2 -1
View File
@@ -5,6 +5,7 @@ import { getLocale } from "@calcom/features/auth/lib/getLocale";
import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages"; import type { AppImageProps, MeetingImageProps } from "@calcom/lib/OgImages";
import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages"; import { constructAppImage, constructGenericImage, constructMeetingImage } from "@calcom/lib/OgImages";
import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants"; import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG, CAL_URL } from "@calcom/lib/constants";
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
import { buildCanonical } from "@calcom/lib/next-seo.config"; import { buildCanonical } from "@calcom/lib/next-seo.config";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import { truncateOnWord } from "@calcom/lib/text"; import { truncateOnWord } from "@calcom/lib/text";
@@ -32,7 +33,7 @@ const _generateMetadataWithoutImage = async (
const description = getDescription(t); const description = getDescription(t);
const titleSuffix = `| ${APP_NAME}`; const titleSuffix = `| ${APP_NAME}`;
const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`; const displayedTitle = title.includes(titleSuffix) || hideBranding ? title : `${title} ${titleSuffix}`;
const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL); const metadataBase = new URL(IS_CALCOM ? getCalcomUrl() : WEBAPP_URL);
return { return {
title: title.length === 0 ? APP_NAME : displayedTitle, title: title.length === 0 ? APP_NAME : displayedTitle,
+2 -1
View File
@@ -9,6 +9,7 @@ import Script from "next/script";
import "@calcom/embed-core/src/embed-iframe"; import "@calcom/embed-core/src/embed-iframe";
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants"; import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
import { getCalcomUrl } from "@calcom/lib/getCalcomUrl";
import { buildCanonical } from "@calcom/lib/next-seo.config"; import { buildCanonical } from "@calcom/lib/next-seo.config";
import { IconSprites } from "@calcom/ui/components/icon"; import { IconSprites } from "@calcom/ui/components/icon";
@@ -64,7 +65,7 @@ function PageWrapper(props: AppProps) {
// Set canonical to https://cal.com or self-hosted URL // Set canonical to https://cal.com or self-hosted URL
canonical={ canonical={
IS_CALCOM IS_CALCOM
? buildCanonical({ path, origin: "https://cal.com" }) // cal.com & .dev ? buildCanonical({ path, origin: getCalcomUrl() }) // cal.com & .dev
: buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted : buildCanonical({ path, origin: WEBAPP_URL }) // self-hosted
} }
{...seoConfig.defaultNextSeo} {...seoConfig.defaultNextSeo}
@@ -1,34 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {
error: null,
migrated: null,
userId: session.user.id,
username: session.user.username,
},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
@@ -1,29 +0,0 @@
import type { GetServerSidePropsContext } from "next";
import { getSession } from "next-auth/react";
import { UserPermissionRole } from "@calcom/prisma/enums";
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
const session = await getSession(ctx);
if (!session || !session.user) {
return {
redirect: {
destination: "/login",
permanent: false,
},
};
}
const isAdmin = session.user.role === UserPermissionRole.ADMIN;
if (!isAdmin) {
return {
redirect: {
destination: "/",
permanent: false,
},
};
}
return {
props: {},
};
}
@@ -156,7 +156,7 @@ export function AvailabilityList({ availabilities }: AvailabilityListProps) {
))} ))}
</ul> </ul>
</div> </div>
<div className="text-default mb-16 mt-4 hidden text-center text-sm md:block"> <div className="text-default mb-16 mt-4 block text-center text-sm">
{t("temporarily_out_of_office")}{" "} {t("temporarily_out_of_office")}{" "}
<Link href="settings/my-account/out-of-office" className="underline"> <Link href="settings/my-account/out-of-office" className="underline">
{t("add_a_redirect")} {t("add_a_redirect")}
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@calcom/web", "name": "@calcom/web",
"version": "5.5.18", "version": "5.6.1",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "analyze": "ANALYZE=true next build",
@@ -6,6 +6,7 @@ import { test } from "./lib/fixtures";
import { import {
bookTimeSlot, bookTimeSlot,
confirmReschedule, confirmReschedule,
confirmBooking,
doOnOrgDomain, doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth, selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth, selectSecondAvailableTimeSlotNextMonth,
@@ -98,6 +99,58 @@ test("dynamic booking info prefilled by query params", async ({ page, users }) =
activeState = await listItemLocator.getAttribute("data-active"); activeState = await listItemLocator.getAttribute("data-active");
expect(activeState).toEqual("false"); expect(activeState).toEqual("false");
}); });
test("multiple duration selection updates event length correctly", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await test.step("update event title to include duration placeholder", async () => {
await page.goto("/event-types");
await page.waitForSelector('[data-testid="event-types"]');
await page.click(`text=Multiple duration`);
await page.waitForSelector('[data-testid="event-title"]');
await expect(page.getByTestId("vertical-tab-event_setup_tab_title")).toHaveAttribute(
"aria-current",
"page"
);
await page.getByTestId("vertical-tab-event_advanced_tab_title").click();
await page.fill('[name="eventName"]', "{Event duration} event btwn {Organiser} {Scheduler}");
await page.locator('[data-testid="update-eventtype"]').click();
await page.waitForResponse("/api/trpc/eventTypes/update?batch=1");
});
await page.goto(`/${user.username}/multiple-duration`);
await page.locator('[data-testid="multiple-choice-30mins"]').waitFor({ state: "visible" });
await test.step("verify default 30min duration is selected", async () => {
const duration30 = page.getByTestId("multiple-choice-30mins");
const activeState = await duration30.getAttribute("data-active");
expect(activeState).toEqual("true");
});
await test.step("book with 90min duration and verify title", async () => {
await page.getByTestId("multiple-choice-90mins").click();
const duration90 = page.getByTestId("multiple-choice-90mins");
const activeState = await duration90.getAttribute("data-active");
expect(activeState).toEqual("true");
await selectFirstAvailableTimeSlotNextMonth(page);
await page.fill('[name="name"]', "Test User");
await page.fill('[name="email"]', "test@example.com");
await confirmBooking(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
const bookingTitle = page.locator('[data-testid="booking-title"]');
await expect(bookingTitle).toBeVisible();
const titleText = await bookingTitle.textContent();
expect(titleText).toContain("90");
});
});
// eslint-disable-next-line playwright/no-skipped-test // eslint-disable-next-line playwright/no-skipped-test
test.skip("it contains the right event details", async ({ page }) => { test.skip("it contains the right event details", async ({ page }) => {
const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`); const response = await page.goto(`http://acme.cal.local:3000/owner1+member1`);
+1 -1
View File
@@ -76,7 +76,7 @@ Sometimes, yarn install might fail during deployment on Vercel, in which case, y
<Step title="Override the build command"> <Step title="Override the build command">
Override to: Override to:
``` ```
cd ../.. && yarn build --include-dependencies --no-deps cd ../.. && yarn build --no-deps
``` ```
</Step> </Step>
<Step title="Hit Deploy"> <Step title="Hit Deploy">
+2 -2
View File
@@ -33,8 +33,8 @@ RUN set -eux; \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives && \ rm -rf /var/lib/apt/lists /var/cache/apt/archives && \
yarn config set httpTimeout 1200000 && \ yarn config set httpTimeout 1200000 && \
npx turbo prune --scope=@calcom/web --docker && \ npx turbo prune --filter=@calcom/web --docker && \
npx turbo prune --scope=@calcom/api --docker && \ npx turbo prune --filter=@calcom/api --docker && \
yarn install && \ yarn install && \
yarn turbo run build --filter=@calcom/api yarn turbo run build --filter=@calcom/api
+13 -13
View File
@@ -28,23 +28,23 @@
"edit-app-template": "yarn app-store edit-template", "edit-app-template": "yarn app-store edit-template",
"delete-app-template": "yarn app-store delete-template", "delete-app-template": "yarn app-store delete-template",
"build": "turbo run build --filter=@calcom/web...", "build": "turbo run build --filter=@calcom/web...",
"build:ai": "turbo run build --scope=\"@calcom/ai\"", "build:ai": "turbo run build --filter=\"@calcom/ai\"",
"clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", "clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf",
"db-deploy": "turbo run db-deploy", "db-deploy": "turbo run db-deploy",
"db-seed": "turbo run db-seed", "db-seed": "turbo run db-seed",
"db-studio": "yarn prisma studio", "db-studio": "yarn prisma studio",
"deploy": "turbo run deploy", "deploy": "turbo run deploy",
"dev:all": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\" --scope=\"@calcom/console\"", "dev:all": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\" --filter=\"@calcom/console\"",
"dev:ai": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/ai\"", "dev:ai": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/ai\"",
"dev:api": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\"", "dev:api": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\"",
"dev:api:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/console\"", "dev:api:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/console\"",
"dev:console": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/console\"", "dev:console": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/console\"",
"dev:swagger": "turbo run dev --scope=\"@calcom/api-proxy\" --scope=\"@calcom/api\" --scope=\"@calcom/swagger\"", "dev:swagger": "turbo run dev --filter=\"@calcom/api-proxy\" --filter=\"@calcom/api\" --filter=\"@calcom/swagger\"",
"dev:website": "turbo run dev --scope=\"@calcom/web\" --scope=\"@calcom/website\"", "dev:website": "turbo run dev --filter=\"@calcom/web\" --filter=\"@calcom/website\"",
"dev": "turbo run dev --scope=\"@calcom/web\"", "dev": "turbo run dev --filter=\"@calcom/web\"",
"build-storybook": "turbo run build --scope=\"@calcom/storybook\"", "build-storybook": "turbo run build --filter=\"@calcom/storybook\"",
"dx": "turbo run dx", "dx": "turbo run dx",
"i-dev": "infisical run -- turbo run dev --scope=\"@calcom/web\"", "i-dev": "infisical run -- turbo run dev --filter=\"@calcom/web\"",
"i-dx": "infisical run -- turbo run dx", "i-dx": "infisical run -- turbo run dx",
"i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example", "i-gen-web-example-env": "infisical secrets generate-example-env --tags=web > .env.example",
"i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example", "i-gen-app-store-example-env": "infisical secrets generate-example-env --tags=appstore > .env.appStore.example",
@@ -61,7 +61,7 @@
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"predev": "echo 'Checking env files'", "predev": "echo 'Checking env files'",
"prisma": "yarn workspace @calcom/prisma prisma", "prisma": "yarn workspace @calcom/prisma prisma",
"start": "turbo run start --scope=\"@calcom/web\"", "start": "turbo run start --filter=\"@calcom/web\"",
"tdd": "vitest watch", "tdd": "vitest watch",
"e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web", "e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --project=@calcom/web",
"e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store", "e2e:app-store": "QUICK=true yarn playwright test --project=@calcom/app-store",
@@ -124,7 +124,7 @@
"date-fns-tz": "^3.2.0", "date-fns-tz": "^3.2.0",
"eslint": "^8.34.0", "eslint": "^8.34.0",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
"turbo": "^1.10.1" "turbo": "^2.5.5"
}, },
"resolutions": { "resolutions": {
"types-ramda": "0.29.4", "types-ramda": "0.29.4",
@@ -55,7 +55,7 @@ export const EventMeta = ({
timeZones, timeZones,
children, children,
selectedTimeslot, selectedTimeslot,
roundRobinHideOrgAndTeam roundRobinHideOrgAndTeam,
}: { }: {
event?: Pick< event?: Pick<
BookerEvent, BookerEvent,
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform"; import { useIsPlatform } from "@calcom/atoms/hooks/useIsPlatform";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps"; import { useShouldShowArrows } from "@calcom/features/apps/components/AllApps";
import { useBookerStore } from "@calcom/features/bookings/Booker/store"; import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import type { BookerEvent } from "@calcom/features/bookings/types"; import type { BookerEvent } from "@calcom/features/bookings/types";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames"; import classNames from "@calcom/ui/classNames";
@@ -45,7 +45,7 @@ export const EventDuration = ({
const { t } = useLocale(); const { t } = useLocale();
const itemRefs = useRef<(HTMLLIElement | null)[]>([]); const itemRefs = useRef<(HTMLLIElement | null)[]>([]);
const isPlatform = useIsPlatform(); const isPlatform = useIsPlatform();
const [selectedDuration, setSelectedDuration, state] = useBookerStore((state) => [ const [selectedDuration, setSelectedDuration, state] = useBookerStoreContext((state) => [
state.selectedDuration, state.selectedDuration,
state.setSelectedDuration, state.setSelectedDuration,
state.state, state.state,
@@ -550,6 +550,9 @@ async function handler(
eventType.schedulingType === SchedulingType.ROUND_ROBIN eventType.schedulingType === SchedulingType.ROUND_ROBIN
) { ) {
const bookingRepo = new BookingRepository(prisma); const bookingRepo = new BookingRepository(prisma);
const requiresPayment = !Number.isNaN(paymentAppData.price) && paymentAppData.price > 0;
const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({ const existingBooking = await bookingRepo.getValidBookingFromEventTypeForAttendee({
eventTypeId, eventTypeId,
bookerEmail, bookerEmail,
@@ -559,13 +562,20 @@ async function handler(
}); });
if (existingBooking) { if (existingBooking) {
const hasPayments = existingBooking.payment.length > 0;
const isPaidBooking = existingBooking.paid || !hasPayments;
const shouldShowPaymentForm = requiresPayment && !isPaidBooking;
const firstPayment = shouldShowPaymentForm ? existingBooking.payment[0] : undefined;
const bookingResponse = { const bookingResponse = {
...existingBooking, ...existingBooking,
user: { user: {
...existingBooking.user, ...existingBooking.user,
email: null, email: null,
}, },
paymentRequired: false, paymentRequired: shouldShowPaymentForm,
seatReferenceUid: "", seatReferenceUid: "",
}; };
@@ -574,8 +584,8 @@ async function handler(
luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [], luckyUsers: bookingResponse.userId ? [bookingResponse.userId] : [],
isDryRun, isDryRun,
...(isDryRun ? { troubleshooterData } : {}), ...(isDryRun ? { troubleshooterData } : {}),
paymentUid: undefined, paymentUid: firstPayment?.uid,
paymentId: undefined, paymentId: firstPayment?.id,
}; };
} }
} }
@@ -7,6 +7,8 @@
* *
* They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app. * They don't intend to test what the apps logic should do, but rather test if the apps are called with the correct data. For testing that, once should write tests within each app.
*/ */
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
import { import {
createBookingScenario, createBookingScenario,
getDate, getDate,
@@ -3277,6 +3279,122 @@ describe("handleNewBooking", () => {
}, },
timeout timeout
); );
test(
`Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`,
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(), getStripeAppCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});
const scenarioData = getScenarioData({
eventTypes: [
{
id: 1,
title: "Paid Event",
description: "It's a test Paid Event",
slotInterval: 30,
requiresConfirmation: false,
metadata: {
apps: {
stripe: {
price: 100,
enabled: true,
currency: "usd",
},
},
},
length: 30,
users: [
{
id: 101,
},
],
},
],
organizer,
apps: [TestData.apps["stripe-payment"]],
});
await createBookingScenario(scenarioData);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
const { paymentUid } = mockPaymentApp({
metadataLookupKey: "stripe",
appStoreLookupKey: "stripepayment",
});
mockCalendarToHaveNoBusySlots("googlecalendar");
const mockBookingData = getMockRequestDataForBooking({
data: {
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: "New York" },
},
},
});
const firstBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(firstBooking).toEqual(
expect.objectContaining({
paymentUid: expect.any(String),
paymentRequired: true,
uid: expect.any(String),
})
);
const firstBookingInDb = await prismaMock.booking.findUnique({
where: { uid: firstBooking.uid },
include: { payment: true },
});
expect(firstBookingInDb).toBeTruthy();
expect(firstBookingInDb?.payment).toHaveLength(1);
expect(firstBookingInDb?.paid).toBe(false);
const secondBooking = await handleNewBooking({
bookingData: mockBookingData,
});
expect(secondBooking.uid).toBe(firstBooking.uid);
expect(secondBooking.paymentUid).toBe(firstBooking.paymentUid);
expect(secondBooking.paymentRequired).toBe(true);
const bookingsInDb = await prismaMock.booking.findMany({
where: {
eventTypeId: 1,
attendees: {
some: {
email: booker.email,
},
},
},
include: { payment: true },
});
expect(bookingsInDb).toHaveLength(1);
expect(bookingsInDb[0].payment).toHaveLength(1);
expect(bookingsInDb[0].uid).toBe(firstBooking.uid);
},
timeout
);
}); });
}); });
@@ -1,11 +1,7 @@
import removeMember from "@calcom/features/ee/teams/lib/removeMember"; import { TeamService } from "@calcom/lib/server/service/teamService";
const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => { const removeUserFromOrg = async ({ userId, orgId }: { userId: number; orgId: number }) => {
return removeMember({ return TeamService.removeMembers({ teamIds: [orgId], userIds: [userId], isOrg: true });
memberId: userId,
teamId: orgId,
isOrg: true,
});
}; };
export default removeUserFromOrg; export default removeUserFromOrg;
@@ -1,141 +0,0 @@
import logger from "@calcom/lib/logger";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import prisma from "@calcom/prisma";
import { TRPCError } from "@trpc/server";
import { deleteWorkfowRemindersOfRemovedMember } from "./deleteWorkflowRemindersOfRemovedMember";
const log = logger.getSubLogger({ prefix: ["removeMember"] });
const removeMember = async ({
memberId,
teamId,
isOrg,
}: {
memberId: number;
teamId: number;
isOrg: boolean;
}) => {
const [membership] = await prisma.$transaction([
prisma.membership.delete({
where: {
userId_teamId: { userId: memberId, teamId: teamId },
},
include: {
user: true,
team: true,
},
}),
// remove user as host from team events associated with this membership
prisma.host.deleteMany({
where: {
userId: memberId,
eventType: {
teamId: teamId,
},
},
}),
]);
const team = await prisma.team.findUnique({
where: { id: teamId },
select: {
isOrganization: true,
organizationSettings: true,
id: true,
metadata: true,
activeOrgWorkflows: true,
parentId: true,
},
});
const foundUser = await prisma.user.findUnique({
where: { id: memberId },
select: {
id: true,
movedToProfileId: true,
email: true,
username: true,
completedOnboarding: true,
teams: {
select: {
team: {
select: {
id: true,
parentId: true,
},
},
},
},
},
});
if (!team || !foundUser) throw new TRPCError({ code: "NOT_FOUND" });
if (isOrg) {
log.debug("Removing a member from the organization");
// Deleting membership from all child teams
// Delete all sub-team memberships where this team is the organization
await prisma.membership.deleteMany({
where: {
team: {
parentId: teamId,
},
userId: membership.userId,
},
});
const userToDeleteMembershipOf = foundUser;
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
userId: userToDeleteMembershipOf.id,
organizationId: team.id,
});
if (
userToDeleteMembershipOf.username &&
userToDeleteMembershipOf.movedToProfileId === profileToDelete?.id
) {
log.debug("Cleaning up tempOrgRedirect for user", userToDeleteMembershipOf.username);
await prisma.tempOrgRedirect.deleteMany({
where: {
from: userToDeleteMembershipOf.username,
},
});
}
await prisma.$transaction([
prisma.user.update({
where: { id: membership.userId },
data: { organizationId: null },
}),
ProfileRepository.delete({
userId: membership.userId,
organizationId: team.id,
}),
prisma.host.deleteMany({
where: {
userId: memberId,
eventType: {
team: {
parentId: teamId,
},
},
},
}),
]);
}
// Deleted managed event types from this team from this member
await prisma.eventType.deleteMany({
where: { parent: { teamId: teamId }, userId: membership.userId },
});
await deleteWorkfowRemindersOfRemovedMember(team, memberId, isOrg);
return { membership };
};
export default removeMember;
@@ -110,13 +110,15 @@ describe("RoleManagementFactory", () => {
it("should allow role change when user has permission", async () => { it("should allow role change when user has permission", async () => {
mockPermissionCheckService.checkPermission.mockResolvedValue(true); mockPermissionCheckService.checkPermission.mockResolvedValue(true);
const manager = await factory.createRoleManager(organizationId); const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow(); await expect(
manager.checkPermissionToChangeRole(userId, organizationId, "org")
).resolves.not.toThrow();
}); });
it("should throw UNAUTHORIZED when user lacks permission", async () => { it("should throw UNAUTHORIZED when user lacks permission", async () => {
mockPermissionCheckService.checkPermission.mockResolvedValue(false); mockPermissionCheckService.checkPermission.mockResolvedValue(false);
const manager = await factory.createRoleManager(organizationId); const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow( await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
new RoleManagementError( new RoleManagementError(
"You do not have permission to change roles", "You do not have permission to change roles",
RoleManagementErrorCode.UNAUTHORIZED RoleManagementErrorCode.UNAUTHORIZED
@@ -193,13 +195,15 @@ describe("RoleManagementFactory", () => {
customRoleId: null, customRoleId: null,
}); });
const manager = await factory.createRoleManager(organizationId); const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).resolves.not.toThrow(); await expect(
manager.checkPermissionToChangeRole(userId, organizationId, "org")
).resolves.not.toThrow();
}); });
it("should throw UNAUTHORIZED when user is not owner", async () => { it("should throw UNAUTHORIZED when user is not owner", async () => {
vi.mocked(isOrganisationAdmin).mockResolvedValue(false); vi.mocked(isOrganisationAdmin).mockResolvedValue(false);
const manager = await factory.createRoleManager(organizationId); const manager = await factory.createRoleManager(organizationId);
await expect(manager.checkPermissionToChangeRole(userId, organizationId)).rejects.toThrow( await expect(manager.checkPermissionToChangeRole(userId, organizationId, "org")).rejects.toThrow(
new RoleManagementError( new RoleManagementError(
"Only owners or admin can update roles", "Only owners or admin can update roles",
RoleManagementErrorCode.UNAUTHORIZED RoleManagementErrorCode.UNAUTHORIZED
@@ -1,5 +1,6 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository"; import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
@@ -10,7 +11,7 @@ import { RoleService } from "./role.service";
interface IRoleManager { interface IRoleManager {
isPBACEnabled: boolean; isPBACEnabled: boolean;
checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void>; checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void>;
assignRole( assignRole(
userId: number, userId: number,
organizationId: number, organizationId: number,
@@ -29,11 +30,11 @@ class PBACRoleManager implements IRoleManager {
private readonly permissionCheckService: PermissionCheckService private readonly permissionCheckService: PermissionCheckService
) {} ) {}
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> { async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
const hasPermission = await this.permissionCheckService.checkPermission({ const hasPermission = await this.permissionCheckService.checkPermission({
userId, userId,
teamId: organizationId, teamId: targetId,
permission: "organization.changeMemberRole", permission: scope === "team" ? "team.changeMemberRole" : "organization.changeMemberRole",
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
}); });
@@ -95,9 +96,11 @@ class PBACRoleManager implements IRoleManager {
class LegacyRoleManager implements IRoleManager { class LegacyRoleManager implements IRoleManager {
public isPBACEnabled = false; public isPBACEnabled = false;
async checkPermissionToChangeRole(userId: number, organizationId: number): Promise<void> { async checkPermissionToChangeRole(userId: number, targetId: number, scope: "org" | "team"): Promise<void> {
const membership = await isOrganisationAdmin(userId, organizationId); const membership =
scope === "team"
? !!(await isTeamAdmin(userId, targetId))
: !!(await isOrganisationAdmin(userId, targetId));
// Only OWNER/ADMIN can update role // Only OWNER/ADMIN can update role
if (!membership) { if (!membership) {
throw new RoleManagementError( throw new RoleManagementError(
+2 -1
View File
@@ -45,7 +45,8 @@ export const IS_CALCOM =
(new URL(WEBAPP_URL).hostname.endsWith("cal.com") || (new URL(WEBAPP_URL).hostname.endsWith("cal.com") ||
new URL(WEBAPP_URL).hostname.endsWith("cal.dev") || new URL(WEBAPP_URL).hostname.endsWith("cal.dev") ||
new URL(WEBAPP_URL).hostname.endsWith("cal.qa") || new URL(WEBAPP_URL).hostname.endsWith("cal.qa") ||
new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com")); new URL(WEBAPP_URL).hostname.endsWith("cal-staging.com") ||
new URL(WEBAPP_URL).hostname.endsWith("cal.eu"));
export const CONSOLE_URL = export const CONSOLE_URL =
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") || new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") ||
@@ -5,8 +5,8 @@ import type {
InsightsRoutingServicePublicOptions, InsightsRoutingServicePublicOptions,
InsightsRoutingServiceFilterOptions, InsightsRoutingServiceFilterOptions,
InsightsRoutingBaseService, InsightsRoutingBaseService,
} from "@calcom/lib/server/service/insightsRoutingBase"; } from "@calcom/lib/server/service/InsightsRoutingBaseService";
import type { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI"; import type { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
import { prismaModule } from "@calcom/prisma/prisma.module"; import { prismaModule } from "@calcom/prisma/prisma.module";
import { insightsRoutingModule } from "../modules/InsightsRouting"; import { insightsRoutingModule } from "../modules/InsightsRouting";
+2 -2
View File
@@ -1,7 +1,7 @@
import { createModule } from "@evyweb/ioctopus"; import { createModule } from "@evyweb/ioctopus";
import type { IInsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI"; import type { IInsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
import { InsightsRoutingService } from "@calcom/lib/server/service/insightsRoutingDI"; import { InsightsRoutingService } from "@calcom/lib/server/service/InsightsRoutingDIService";
import { DI_TOKENS } from "../tokens"; import { DI_TOKENS } from "../tokens";
+8
View File
@@ -0,0 +1,8 @@
import { WEBAPP_URL, IS_CALCOM } from "./constants";
export const getCalcomUrl = () => {
if (IS_CALCOM) {
return new URL(WEBAPP_URL).hostname.endsWith("cal.eu") ? "https://cal.eu" : "https://cal.com";
}
return WEBAPP_URL;
};
@@ -828,6 +828,7 @@ export class BookingRepository {
attendees: true, attendees: true,
references: true, references: true,
user: true, user: true,
payment: true,
}, },
}); });
} }
@@ -4,7 +4,7 @@ import {
InsightsRoutingBaseService, InsightsRoutingBaseService,
type InsightsRoutingServicePublicOptions, type InsightsRoutingServicePublicOptions,
type InsightsRoutingServiceFilterOptions, type InsightsRoutingServiceFilterOptions,
} from "./insightsRoutingBase"; } from "./InsightsRoutingBaseService";
export interface IInsightsRoutingService { export interface IInsightsRoutingService {
prisma: typeof readonlyPrisma; prisma: typeof readonlyPrisma;
@@ -7,7 +7,7 @@ import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import { BookingStatus, MembershipRole } from "@calcom/prisma/enums"; import { BookingStatus, MembershipRole } from "@calcom/prisma/enums";
import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/insightsRoutingBase"; import { InsightsRoutingBaseService as InsightsRoutingService } from "../../service/InsightsRoutingBaseService";
// SQL condition constants for testing // SQL condition constants for testing
const NOTHING_CONDITION = Prisma.sql`1=0`; const NOTHING_CONDITION = Prisma.sql`1=0`;
File diff suppressed because it is too large Load Diff
+263 -6
View File
@@ -1,16 +1,52 @@
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { TeamBilling } from "@calcom/features/ee/billing/teams"; import { TeamBilling } from "@calcom/features/ee/billing/teams";
import removeMember from "@calcom/features/ee/teams/lib/removeMember"; import { deleteWorkfowRemindersOfRemovedMember } from "@calcom/features/ee/teams/lib/deleteWorkflowRemindersOfRemovedMember";
import { deleteDomain } from "@calcom/lib/domainManager/organization"; import { deleteDomain } from "@calcom/lib/domainManager/organization";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import { ProfileRepository } from "@calcom/lib/server/repository/profile";
import { TeamRepository } from "@calcom/lib/server/repository/team"; import { TeamRepository } from "@calcom/lib/server/repository/team";
import { WorkflowService } from "@calcom/lib/server/service/workflows"; import { WorkflowService } from "@calcom/lib/server/service/workflows";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
import type { Membership } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
const log = logger.getSubLogger({ prefix: ["TeamService"] });
type MembershipWithRelations = Pick<
Membership,
"id" | "userId" | "teamId" | "role" | "accepted" | "disableImpersonation"
>;
type TeamWithSettings = {
id: number;
isOrganization: boolean | null;
organizationSettings: unknown;
metadata: unknown;
activeOrgWorkflows: unknown;
parentId: number | null;
};
type UserWithTeams = {
id: number;
movedToProfileId: number | null;
email: string;
username: string | null;
completedOnboarding: boolean;
teams: {
team: {
id: number;
parentId: number | null;
};
}[];
};
export type RemoveMemberResult = {
membership: MembershipWithRelations;
};
export class TeamService { export class TeamService {
/** /**
* Deletes a team and all its associated data in a safe, transactional order. * Deletes a team and all its associated data in a safe, transactional order.
@@ -45,15 +81,23 @@ export class TeamService {
return deletedTeam; return deletedTeam;
} }
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) { static async removeMembers({
const deleteMembershipPromises = []; teamIds,
userIds,
isOrg = false,
}: {
teamIds: number[];
userIds: number[];
isOrg?: boolean;
}) {
const deleteMembershipPromises: Promise<RemoveMemberResult>[] = [];
for (const memberId of memberIds) { for (const userId of userIds) {
for (const teamId of teamIds) { for (const teamId of teamIds) {
deleteMembershipPromises.push( deleteMembershipPromises.push(
removeMember({ TeamService.removeMember({
teamId, teamId,
memberId, userId,
isOrg, isOrg,
}) })
); );
@@ -121,4 +165,217 @@ export class TeamService {
const teamBilling = await TeamBilling.findAndInit(teamId); const teamBilling = await TeamBilling.findAndInit(teamId);
return teamBilling.publish(); return teamBilling.publish();
} }
private static async removeMember({
userId,
teamId,
isOrg,
}: {
userId: number;
teamId: number;
isOrg: boolean;
}) {
const membership = await TeamService.fetchMembershipOrThrow(userId, teamId);
const team = await TeamService.fetchTeamOrThrow(teamId);
const user = await TeamService.fetchUserOrThrow(userId);
if (isOrg) {
log.debug("Removing a member from the organization");
await TeamService.removeFromOrganization(membership, team, user);
} else {
log.debug("Removing a member from a team");
await TeamService.removeFromTeam(membership, teamId);
}
await deleteWorkfowRemindersOfRemovedMember(team, userId, isOrg);
return { membership };
}
// TODO: Needs to be moved to repository
private static async fetchMembershipOrThrow(
userId: number,
teamId: number
): Promise<MembershipWithRelations> {
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: { userId: userId, teamId: teamId },
},
select: {
id: true,
userId: true,
teamId: true,
role: true,
accepted: true,
disableImpersonation: true,
},
});
if (!membership) {
throw new TRPCError({ code: "NOT_FOUND", message: "Membership not found" });
}
return membership;
}
// TODO: Needs to be moved to repository
private static async fetchTeamOrThrow(teamId: number): Promise<TeamWithSettings> {
const team = await prisma.team.findUnique({
where: { id: teamId },
select: {
isOrganization: true,
organizationSettings: true,
id: true,
metadata: true,
activeOrgWorkflows: true,
parentId: true,
},
});
if (!team) {
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
}
return team;
}
// TODO: Needs to be moved to repository
private static async fetchUserOrThrow(userId: number): Promise<UserWithTeams> {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
movedToProfileId: true,
email: true,
username: true,
completedOnboarding: true,
teams: {
select: {
team: {
select: {
id: true,
parentId: true,
},
},
},
},
},
});
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
}
return user;
}
// TODO: Needs to be moved to repository
private static async cleanupTempOrgRedirect(user: UserWithTeams, team: TeamWithSettings) {
const profileToDelete = await ProfileRepository.findByUserIdAndOrgId({
userId: user.id,
organizationId: team.id,
});
if (user.username && user.movedToProfileId === profileToDelete?.id) {
log.debug("Cleaning up tempOrgRedirect for user", user.username);
await prisma.tempOrgRedirect.deleteMany({
where: {
from: user.username,
},
});
}
}
private static async removeFromOrganization(
membership: MembershipWithRelations,
team: TeamWithSettings,
user: UserWithTeams
) {
await TeamService.cleanupTempOrgRedirect(user, team);
const newUsername = generateNewUsername(user);
await prisma.$transaction([
// Remove user from all sub-teams event type hosts
prisma.host.deleteMany({
where: {
userId: membership.userId,
eventType: {
team: {
parentId: team.id,
},
},
},
}),
// Delete managed child events in sub-teams
prisma.eventType.deleteMany({
where: {
userId: membership.userId,
parent: {
team: {
parentId: team.id,
},
},
},
}),
// Remove organizationId from the user
prisma.user.update({
where: { id: membership.userId },
data: {
organizationId: null,
username: newUsername,
},
}),
// Delete the profile of the user from the organization
ProfileRepository.delete({
userId: membership.userId,
organizationId: team.id,
}),
// Delete all sub-team memberships where this team is the organization
prisma.membership.deleteMany({
where: {
team: {
parentId: team.id,
},
userId: membership.userId,
},
}),
// Delete the membership of the user from the organization
prisma.membership.delete({
where: {
userId_teamId: { userId: membership.userId, teamId: team.id },
},
}),
]);
// Generate new username for user leaving organization
function generateNewUsername(user: UserWithTeams): string | null {
// We ensure that new username would be unique across all users in the global namespace outside any organization
return user.username != null ? `${user.username}-${user.id}` : null;
}
}
// Remove member from regular team
private static async removeFromTeam(membership: MembershipWithRelations, teamId: number) {
await prisma.$transaction([
// Remove user from all team event types' hosts
prisma.host.deleteMany({
where: {
userId: membership.userId,
eventType: {
teamId: teamId,
},
},
}),
// Deleted managed event types from this team for this member
prisma.eventType.deleteMany({
where: { parent: { teamId: teamId }, userId: membership.userId },
}),
// Delete the membership of the user from the team
prisma.membership.delete({
where: {
userId_teamId: { userId: membership.userId, teamId: teamId },
},
}),
]);
}
} }
+1
View File
@@ -120,4 +120,5 @@ export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
export { verifyPhoneNumber, sendVerificationCode }; export { verifyPhoneNumber, sendVerificationCode };
export { TeamService } from "@calcom/lib/server/service/teamService";
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache"; export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
Binary file not shown.
@@ -47,7 +47,7 @@ export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId); const roleManager = await RoleManagementFactory.getInstance().createRoleManager(organizationId);
try { try {
await roleManager.checkPermissionToChangeRole(userId, organizationId); await roleManager.checkPermissionToChangeRole(userId, organizationId, "org");
} catch (error) { } catch (error) {
if (error instanceof RoleManagementError) { if (error instanceof RoleManagementError) {
throw new TRPCError({ code: "UNAUTHORIZED", message: error.message }); throw new TRPCError({ code: "UNAUTHORIZED", message: error.message });
@@ -1,5 +1,5 @@
import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory"; import { RoleManagementFactory } from "@calcom/features/pbac/services/role-management.factory";
import { isTeamAdmin, isTeamOwner } from "@calcom/lib/server/queries/teams"; import { isTeamOwner } from "@calcom/lib/server/queries/teams";
import { TeamRepository } from "@calcom/lib/server/repository/team"; import { TeamRepository } from "@calcom/lib/server/repository/team";
import { prisma } from "@calcom/prisma"; import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums"; import { MembershipRole } from "@calcom/prisma/enums";
@@ -32,7 +32,7 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
// Check permission to change roles // Check permission to change roles
try { try {
await roleManager.checkPermissionToChangeRole(ctx.user.id, organizationId); await roleManager.checkPermissionToChangeRole(ctx.user.id, input.teamId, "team");
} catch (error) { } catch (error) {
throw new TRPCError({ throw new TRPCError({
code: "UNAUTHORIZED", code: "UNAUTHORIZED",
@@ -45,8 +45,6 @@ export const changeMemberRoleHandler = async ({ ctx, input }: ChangeMemberRoleOp
typeof input.role === "string" && typeof input.role === "string" &&
Object.values(MembershipRole).includes(input.role as MembershipRole) Object.values(MembershipRole).includes(input.role as MembershipRole)
) { ) {
// Traditional role assignment logic
if (!(await isTeamAdmin(ctx.user?.id, input.teamId))) throw new TRPCError({ code: "UNAUTHORIZED" });
// Only owners can award owner role. // Only owners can award owner role.
if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId))) if (input.role === MembershipRole.OWNER && !(await isTeamOwner(ctx.user?.id, input.teamId)))
throw new TRPCError({ code: "UNAUTHORIZED" }); throw new TRPCError({ code: "UNAUTHORIZED" });
@@ -59,7 +59,7 @@ export const removeMemberHandler = async ({ ctx, input }: RemoveMemberOptions) =
message: "You can not remove yourself from a team you own.", message: "You can not remove yourself from a team you own.",
}); });
await TeamService.removeMembers(teamIds, memberIds, isOrg); await TeamService.removeMembers({ teamIds, userIds: memberIds, isOrg });
}; };
export default removeMemberHandler; export default removeMemberHandler;
@@ -1,167 +0,0 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
// @ts-nocheck
// TODO: Bring this test back with the correct setup (no illegal imports)
import prismaMock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, test, expect } from "vitest";
import { SchedulingType, MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "../../../types";
import removeMember from "./removeMember.handler";
describe.skip("removeMember", () => {
describe("should remove a member from a team", () => {
test(`1) Should remove a member from a team
2) Should remove the member from hosts
`, async () => {
const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});
// Create the child team
const childTeam = {
id: 202,
name: "Team 1",
slug: "team-1",
parentId: org.id,
};
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
organizationId: org.id,
schedules: [TestData.schedules.IstWorkHours],
teams: [
{
membership: {
accepted: true,
role: MembershipRole.ADMIN,
},
team: {
id: org.id,
name: "Test Org",
slug: "testorg",
},
},
{
membership: {
accepted: true,
role: MembershipRole.ADMIN,
},
team: {
id: childTeam.id,
name: "Team 1",
slug: "team-1",
parentId: org.id,
},
},
],
});
const otherTeamMembers = [
{
name: "Other Team Member 1",
username: "other-team-member-1",
timeZone: Timezones["+5:30"],
defaultScheduleId: null,
email: "other-team-member-1@example.com",
id: 102,
organizationId: org.id,
schedules: [TestData.schedules.IstEveningShift],
teams: [
{
membership: {
accepted: true,
role: MembershipRole.MEMBER,
},
team: {
id: org.id,
name: "Test Org",
slug: "testorg",
},
},
{
membership: {
accepted: true,
role: MembershipRole.MEMBER,
},
team: {
id: childTeam.id,
name: "Team 1",
slug: "team-1",
parentId: org.id,
},
},
],
},
];
await createBookingScenario(
getScenarioData(
{
eventTypes: [
{
id: 1,
teamId: childTeam.id,
schedulingType: SchedulingType.ROUND_ROBIN,
length: 30,
hosts: [
{
userId: 101,
isFixed: false,
},
{
userId: 102,
isFixed: false,
},
],
},
],
organizer,
usersApartFromOrganizer: otherTeamMembers,
apps: [TestData.apps["daily-video"]],
},
org
)
);
//Logged in user is admin of the org
const ctx = {
user: {
id: organizer.id,
name: organizer.name,
} as NonNullable<TrpcSessionUser>,
};
await removeMember({
ctx,
input: {
teamIds: [org.id],
memberIds: [102],
isOrg: true,
},
});
//Check if the remaining memberships are correct
const remainingMemberships = await prismaMock.membership.count({
where: {
teamId: childTeam.id,
},
});
expect(remainingMemberships).toBe(1);
//Check if the event type has the correct hosts
const remainingHostsCount = await prismaMock.host.count({
where: {
eventTypeId: 1,
},
});
expect(remainingHostsCount).toBe(1);
});
});
});
+4 -15
View File
@@ -1,19 +1,5 @@
{ {
"extends": "@calcom/tsconfig/react-library.json", "extends": "@calcom/tsconfig/react-library.json",
"paths": {
"~/*": ["/*"],
"@calcom/app-store": ["../app-store"],
"@calcom/app-store/*": ["../app-store/*"],
"@calcom/dayjs": ["../dayjs"],
"@calcom/dayjs/*": ["../dayjs/*"],
"@calcom/features": ["../features"],
"@calcom/features/*": ["../features/*"],
"@calcom/lib": ["../lib"],
"@calcom/lib/*": ["../lib/*"],
"@calcom/prisma": ["../prisma"],
"@calcom/prisma/*": ["../prisma/*"],
"@calcom/repository/*": ["../lib/server/repository/*"]
},
"include": [ "include": [
"../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",
@@ -33,6 +19,9 @@
"declarationMap": false, "declarationMap": false,
"resolveJsonModule": true, "resolveJsonModule": true,
"declarationDir": "types/server", "declarationDir": "types/server",
"moduleResolution": "node" "moduleResolution": "node",
"paths": {
"~/*": ["./*"]
}
} }
} }
+254 -253
View File
@@ -1,6 +1,257 @@
{ {
"$schema": "https://turborepo.org/schema.json", "$schema": "https://turborepo.org/schema.json",
"pipeline": { "globalDependencies": ["yarn.lock"],
"globalEnv": [
"ALLOWED_HOSTNAMES",
"ANALYZE",
"API_KEY_PREFIX",
"APP_USER_NAME",
"ATOMS_E2E_API_URL",
"ATOMS_E2E_OAUTH_CLIENT_ID",
"ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED",
"ATOMS_E2E_OAUTH_CLIENT_SECRET",
"ATOMS_E2E_ORG_ID",
"BASECAMP3_CLIENT_ID",
"BASECAMP3_CLIENT_SECRET",
"BASECAMP3_USER_AGENT",
"BLACKLISTED_GUEST_EMAILS",
"AUTH_BEARER_TOKEN_VERCEL",
"BUILD_ID",
"CAL_SIGNATURE_TOKEN",
"CALCOM_PRIVATE_API_ROUTE",
"CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
"CALCOM_CREDENTIAL_SYNC_SECRET",
"CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME",
"CALCOM_ENV",
"CALCOM_LICENSE_KEY",
"CALCOM_QA_EMAIL",
"CALCOM_QA_PASSWORD",
"CALCOM_TELEMETRY_DISABLED",
"CALCOM_CREDENTIAL_SYNC_HEADER_NAME",
"CALENDSO_ENCRYPTION_KEY",
"CALCOM_CREDENTIAL_SYNC_SECRET",
"CI",
"CLOSECOM_CLIENT_ID",
"CLOSECOM_CLIENT_SECRET",
"CRON_API_KEY",
"CRON_SECRET",
"CRON_ENABLE_APP_SYNC",
"CLOUDFLARE_TURNSTILE_SECRET",
"DAILY_API_KEY",
"DAILY_SCALE_PLAN",
"DAILY_WEBHOOK_SECRET",
"DAILY_MEETING_ENDED_WEBHOOK_SECRET",
"DAILY_VIDEO_REGION",
"DATABASE_CHUNK_SIZE",
"DATABASE_DIRECT_URL",
"DATABASE_URL",
"DEBUG",
"DUB_API_KEY",
"NEXT_PUBLIC_DUB_PROGRAM_ID",
"E2E_TEST_APPLE_CALENDAR_EMAIL",
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
"E2E_TEST_CALCOM_QA_EMAIL",
"E2E_TEST_CALCOM_QA_PASSWORD",
"E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS",
"E2E_TEST_CALCOM_GCAL_KEYS",
"E2E_TEST_MAILHOG_ENABLED",
"E2E_TEST_OIDC_CLIENT_ID",
"E2E_TEST_OIDC_CLIENT_SECRET",
"E2E_TEST_OIDC_PROVIDER_DOMAIN",
"E2E_TEST_OIDC_USER_EMAIL",
"E2E_TEST_OIDC_USER_PASSWORD",
"E2E_TEST_SAML_ADMIN_EMAIL",
"E2E_TEST_SAML_ADMIN_PASSWORD",
"EMAIL_FROM",
"EMAIL_FROM_NAME",
"EMAIL_SERVER_HOST",
"EMAIL_SERVER_PASSWORD",
"EMAIL_SERVER_PORT",
"EMAIL_SERVER_USER",
"EMAIL_SERVER",
"ENABLE_EXPANDED_CACHE",
"EXCHANGE_DEFAULT_EWS_URL",
"FORMBRICKS_FEEDBACK_SURVEY_ID",
"AVATARAPI_USERNAME",
"AVATARAPI_PASSWORD",
"GIPHY_API_KEY",
"GITHUB_API_REPO_TOKEN",
"GOOGLE_API_CREDENTIALS",
"GOOGLE_LOGIN_ENABLED",
"GOOGLE_WEBHOOK_TOKEN",
"HEROKU_APP_NAME",
"HUBSPOT_CLIENT_ID",
"HUBSPOT_CLIENT_SECRET",
"IFFY_API_KEY",
"INTEGRATION_TEST_MODE",
"IS_E2E",
"INTERCOM_SECRET",
"INTERCOM_SECRET",
"INSIGHTS_DATABASE_URL",
"IP_BANLIST",
"LARK_OPEN_APP_ID",
"LARK_OPEN_APP_SECRET",
"LARK_OPEN_VERIFICATION_TOKEN",
"MOCK_PAYMENT_APP_ENABLED",
"MS_GRAPH_CLIENT_ID",
"MS_GRAPH_CLIENT_SECRET",
"NEXT_PUBLIC_API_URL",
"NEXT_PUBLIC_APP_NAME",
"NEXT_PUBLIC_AUTH_URL",
"NEXT_PUBLIC_CALCOM_VERSION",
"NEXT_PUBLIC_COMPANY_NAME",
"NEXT_PUBLIC_CONSOLE_URL",
"NEXT_PUBLIC_LOGGER_LEVEL",
"NEXT_PUBLIC_DISABLE_SIGNUP",
"NEXT_PUBLIC_EMBED_LIB_URL",
"NEXT_PUBLIC_FORMBRICKS_HOST_URL",
"NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID",
"NEXT_PUBLIC_HOSTED_CAL_FEATURES",
"NEXT_PUBLIC_MINUTES_TO_BOOK",
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
"NEXT_PUBLIC_SENDER_ID",
"NEXT_PUBLIC_SENDGRID_SENDER_NAME",
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID",
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_PUBLISHABLE_KEY",
"NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS",
"NEXT_PUBLIC_SWAGGER_DOCS_URL",
"NEXT_PUBLIC_TEAM_IMPERSONATION",
"NEXT_PUBLIC_VERCEL_URL",
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
"NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER",
"NEXT_PUBLIC_VERCEL_ENV",
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
"NEXT_PUBLIC_GTM_ID",
"NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS",
"NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW",
"NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL",
"NEXT_PUBLIC_WEBSITE_TERMS_URL",
"NEXT_RUNTIME",
"NEXTAUTH_COOKIE_DOMAIN",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"NODE_ENV",
"ORGANIZATIONS_ENABLED",
"ORGANIZATIONS_AUTOLINK",
"PAYMENT_FEE_FIXED",
"PAYMENT_FEE_PERCENTAGE",
"PLAYWRIGHT_HEADLESS",
"PLAYWRIGHT_TEST_BASE_URL",
"PRISMA_FIELD_ENCRYPTION_KEY",
"PRISMA_GENERATE_DATAPROXY",
"PROJECT_ID_VERCEL",
"QUICK",
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"RESERVED_SUBDOMAINS",
"RETELL_AI_KEY",
"SALESFORCE_CONSUMER_KEY",
"SALESFORCE_CONSUMER_SECRET",
"SAML_ADMINS",
"SAML_CLIENT_SECRET_VERIFIER",
"SAML_DATABASE_URL",
"SEND_FEEDBACK_EMAIL",
"SENDGRID_API_KEY",
"SENDGRID_EMAIL",
"SENDGRID_SYNC_API_KEY",
"SKIP_DB_MIGRATIONS",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",
"SLACK_SIGNING_SECRET",
"STRIPE_CLIENT_ID",
"STRIPE_PRIVATE_KEY",
"STRIPE_PRODUCT_ID_ESSENTIALS",
"STRIPE_PRODUCT_ID_SCALE",
"STRIPE_PRODUCT_ID_STARTER",
"STRIPE_WEBHOOK_SECRET",
"STRIPE_WEBHOOK_SECRET_APPS",
"STRIPE_WEBHOOK_SECRET_BILLING",
"STRIPE_TEAM_MONTHLY_PRICE_ID",
"TANDEM_BASE_URL",
"TANDEM_CLIENT_ID",
"TANDEM_CLIENT_SECRET",
"TASKER_ENABLE_WEBHOOKS",
"TEAM_ID_VERCEL",
"TELEMETRY_DEBUG",
"TWILIO_MESSAGING_SID",
"TWILIO_OPT_OUT_ENABLED",
"TWILIO_PHONE_NUMBER",
"TWILIO_WHATSAPP_PHONE_NUMBER",
"TWILIO_SID",
"TWILIO_TOKEN",
"TWILIO_VERIFY_SID",
"TWILIO_WHATSAPP_REMINDER_CONTENT_SID",
"TWILIO_WHATSAPP_CANCELLED_CONTENT_SID",
"TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID",
"TWILIO_WHATSAPP_COMPLETED_CONTENT_SID",
"UPSTASH_REDIS_REST_TOKEN",
"UPSTASH_REDIS_REST_URL",
"UNKEY_ROOT_KEY",
"USERNAME_BLACKLIST_URL",
"VERCEL_ENV",
"VERCEL_URL",
"VITAL_API_KEY",
"VITAL_DEVELOPMENT_MODE",
"VITAL_REGION",
"VITAL_WEBHOOK_SECRET",
"ZAPIER_INVITE_LINK",
"ZOHOCRM_CLIENT_ID",
"ZOHOCRM_CLIENT_SECRET",
"ZOOM_CLIENT_ID",
"ZOOM_CLIENT_SECRET",
"REVERT_API_KEY",
"REVERT_API_URL",
"REVERT_PUBLIC_TOKEN",
"RESEND_API_KEY",
"LOCAL_TESTING_DOMAIN_VERCEL",
"AUTH_BEARER_TOKEN_CLOUDFLARE",
"CLOUDFLARE_ZONE_ID",
"NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY",
"NEXT_PUBLIC_CHAT_API_URL",
"NEXT_PUBLIC_DOCS_URL",
"CLOUDFLARE_VERCEL_CNAME",
"CLOUDFLARE_DNS",
"EMBED_PUBLIC_EMBED_FINGER_PRINT",
"EMBED_PUBLIC_EMBED_VERSION",
"EMBED_PUBLIC_WEBAPP_URL",
"EMBED_PUBLIC_VERCEL_URL",
"EMBED_PUBLIC_EMBED_LIB_URL",
"NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER",
"NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS",
"NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS",
"NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS",
"NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM",
"NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT",
"NEXT_PUBLIC_HEAD_SCRIPTS",
"NEXT_PUBLIC_BODY_SCRIPTS",
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
"NEXT_PUBLIC_API_V2_ROOT_URL",
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
"VAPID_PRIVATE_KEY",
"CAL_VIDEO_BUCKET_NAME",
"CAL_VIDEO_BUCKET_REGION",
"CAL_VIDEO_ASSUME_ROLE_ARN",
"CAL_VIDEO_MEETING_LINK_FOR_TESTING",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"VAPID_PRIVATE_KEY",
"HUDDLE01_API_TOKEN",
"LINGO_DOT_DEV_API_KEY",
"DIRECTORY_IDS_TO_LOG",
"NEXT_PUBLIC_SINGLE_ORG_SLUG",
"GOOGLE_REFRESH_TOKEN",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"ZOOM_REFRESH_TOKEN",
"CALCOM_ADMIN_API_KEY",
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
"ORGANIZER_EMAIL_EXEMPT_DOMAINS",
"SLOTS_CACHE_TTL"
],
"tasks": {
"@calcom/prisma#build": { "@calcom/prisma#build": {
"cache": false, "cache": false,
"dependsOn": ["post-install"] "dependsOn": ["post-install"]
@@ -207,7 +458,7 @@
}, },
"@calcom/app-store-cli#build": { "@calcom/app-store-cli#build": {
"cache": false, "cache": false,
"inputs": ["../../app-store/**/**"], "inputs": ["../../app-store/**"],
"outputs": ["../../app-store/apps.server.generated.ts", "../../app-store/apps.browser.generated.tsx"] "outputs": ["../../app-store/apps.server.generated.ts", "../../app-store/apps.browser.generated.tsx"]
}, },
"@calcom/embed-react#type-check": { "@calcom/embed-react#type-check": {
@@ -247,255 +498,5 @@
"//#test": { "//#test": {
"cache": false "cache": false
} }
}, }
"globalDependencies": ["yarn.lock"],
"globalEnv": [
"ALLOWED_HOSTNAMES",
"ANALYZE",
"API_KEY_PREFIX",
"APP_USER_NAME",
"ATOMS_E2E_API_URL",
"ATOMS_E2E_OAUTH_CLIENT_ID",
"ATOMS_E2E_OAUTH_CLIENT_ID_BOOKER_EMBED",
"ATOMS_E2E_OAUTH_CLIENT_SECRET",
"ATOMS_E2E_ORG_ID",
"BASECAMP3_CLIENT_ID",
"BASECAMP3_CLIENT_SECRET",
"BASECAMP3_USER_AGENT",
"BLACKLISTED_GUEST_EMAILS",
"AUTH_BEARER_TOKEN_VERCEL",
"BUILD_ID",
"CAL_SIGNATURE_TOKEN",
"CALCOM_PRIVATE_API_ROUTE",
"CALCOM_APP_CREDENTIAL_ENCRYPTION_KEY",
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
"CALCOM_CREDENTIAL_SYNC_SECRET",
"CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME",
"CALCOM_ENV",
"CALCOM_LICENSE_KEY",
"CALCOM_QA_EMAIL",
"CALCOM_QA_PASSWORD",
"CALCOM_TELEMETRY_DISABLED",
"CALCOM_CREDENTIAL_SYNC_HEADER_NAME",
"CALENDSO_ENCRYPTION_KEY",
"CALCOM_CREDENTIAL_SYNC_SECRET",
"CI",
"CLOSECOM_CLIENT_ID",
"CLOSECOM_CLIENT_SECRET",
"CRON_API_KEY",
"CRON_SECRET",
"CRON_ENABLE_APP_SYNC",
"CLOUDFLARE_TURNSTILE_SECRET",
"DAILY_API_KEY",
"DAILY_SCALE_PLAN",
"DAILY_WEBHOOK_SECRET",
"DAILY_MEETING_ENDED_WEBHOOK_SECRET",
"DAILY_VIDEO_REGION",
"DATABASE_CHUNK_SIZE",
"DATABASE_DIRECT_URL",
"DATABASE_URL",
"DEBUG",
"DUB_API_KEY",
"NEXT_PUBLIC_DUB_PROGRAM_ID",
"E2E_TEST_APPLE_CALENDAR_EMAIL",
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
"E2E_TEST_CALCOM_QA_EMAIL",
"E2E_TEST_CALCOM_QA_PASSWORD",
"E2E_TEST_CALCOM_QA_GCAL_CREDENTIALS",
"E2E_TEST_CALCOM_GCAL_KEYS",
"E2E_TEST_MAILHOG_ENABLED",
"E2E_TEST_OIDC_CLIENT_ID",
"E2E_TEST_OIDC_CLIENT_SECRET",
"E2E_TEST_OIDC_PROVIDER_DOMAIN",
"E2E_TEST_OIDC_USER_EMAIL",
"E2E_TEST_OIDC_USER_PASSWORD",
"E2E_TEST_SAML_ADMIN_EMAIL",
"E2E_TEST_SAML_ADMIN_PASSWORD",
"EMAIL_FROM",
"EMAIL_FROM_NAME",
"EMAIL_SERVER_HOST",
"EMAIL_SERVER_PASSWORD",
"EMAIL_SERVER_PORT",
"EMAIL_SERVER_USER",
"EMAIL_SERVER",
"ENABLE_EXPANDED_CACHE",
"EXCHANGE_DEFAULT_EWS_URL",
"FORMBRICKS_FEEDBACK_SURVEY_ID",
"AVATARAPI_USERNAME",
"AVATARAPI_PASSWORD",
"GIPHY_API_KEY",
"GITHUB_API_REPO_TOKEN",
"GOOGLE_API_CREDENTIALS",
"GOOGLE_LOGIN_ENABLED",
"GOOGLE_WEBHOOK_TOKEN",
"HEROKU_APP_NAME",
"HUBSPOT_CLIENT_ID",
"HUBSPOT_CLIENT_SECRET",
"IFFY_API_KEY",
"INTEGRATION_TEST_MODE",
"IS_E2E",
"INTERCOM_SECRET",
"INTERCOM_SECRET",
"INSIGHTS_DATABASE_URL",
"IP_BANLIST",
"LARK_OPEN_APP_ID",
"LARK_OPEN_APP_SECRET",
"LARK_OPEN_VERIFICATION_TOKEN",
"MOCK_PAYMENT_APP_ENABLED",
"MS_GRAPH_CLIENT_ID",
"MS_GRAPH_CLIENT_SECRET",
"NEXT_PUBLIC_API_URL",
"NEXT_PUBLIC_APP_NAME",
"NEXT_PUBLIC_AUTH_URL",
"NEXT_PUBLIC_CALCOM_VERSION",
"NEXT_PUBLIC_COMPANY_NAME",
"NEXT_PUBLIC_CONSOLE_URL",
"NEXT_PUBLIC_LOGGER_LEVEL",
"NEXT_PUBLIC_DISABLE_SIGNUP",
"NEXT_PUBLIC_EMBED_LIB_URL",
"NEXT_PUBLIC_FORMBRICKS_HOST_URL",
"NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID",
"NEXT_PUBLIC_HOSTED_CAL_FEATURES",
"NEXT_PUBLIC_MINUTES_TO_BOOK",
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
"NEXT_PUBLIC_SENDER_ID",
"NEXT_PUBLIC_SENDGRID_SENDER_NAME",
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_ID",
"NEXT_PUBLIC_STRIPE_PRICING_TABLE_PUBLISHABLE_KEY",
"NEXT_PUBLIC_SUPPORT_MAIL_ADDRESS",
"NEXT_PUBLIC_SWAGGER_DOCS_URL",
"NEXT_PUBLIC_TEAM_IMPERSONATION",
"NEXT_PUBLIC_VERCEL_URL",
"NEXT_PUBLIC_CLOUDFLARE_SITEKEY",
"NEXT_PUBLIC_CLOUDFLARE_USE_TURNSTILE_IN_BOOKER",
"NEXT_PUBLIC_VERCEL_ENV",
"NEXT_PUBLIC_VERCEL_BRANCH_URL",
"NEXT_PUBLIC_GTM_ID",
"NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS",
"NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW",
"NEXT_PUBLIC_WEBSITE_PRIVACY_POLICY_URL",
"NEXT_PUBLIC_WEBSITE_TERMS_URL",
"NEXT_RUNTIME",
"NEXTAUTH_COOKIE_DOMAIN",
"NEXTAUTH_SECRET",
"NEXTAUTH_URL",
"NODE_ENV",
"ORGANIZATIONS_ENABLED",
"ORGANIZATIONS_AUTOLINK",
"PAYMENT_FEE_FIXED",
"PAYMENT_FEE_PERCENTAGE",
"PLAYWRIGHT_HEADLESS",
"PLAYWRIGHT_TEST_BASE_URL",
"PRISMA_FIELD_ENCRYPTION_KEY",
"PRISMA_GENERATE_DATAPROXY",
"PROJECT_ID_VERCEL",
"QUICK",
"RAILWAY_STATIC_URL",
"RENDER_EXTERNAL_URL",
"RESERVED_SUBDOMAINS",
"RETELL_AI_KEY",
"SALESFORCE_CONSUMER_KEY",
"SALESFORCE_CONSUMER_SECRET",
"SAML_ADMINS",
"SAML_CLIENT_SECRET_VERIFIER",
"SAML_DATABASE_URL",
"SEND_FEEDBACK_EMAIL",
"SENDGRID_API_KEY",
"SENDGRID_EMAIL",
"SENDGRID_SYNC_API_KEY",
"SKIP_DB_MIGRATIONS",
"SLACK_CLIENT_ID",
"SLACK_CLIENT_SECRET",
"SLACK_SIGNING_SECRET",
"STRIPE_CLIENT_ID",
"STRIPE_PRIVATE_KEY",
"STRIPE_PRODUCT_ID_ESSENTIALS",
"STRIPE_PRODUCT_ID_SCALE",
"STRIPE_PRODUCT_ID_STARTER",
"STRIPE_WEBHOOK_SECRET",
"STRIPE_WEBHOOK_SECRET_APPS",
"STRIPE_WEBHOOK_SECRET_BILLING",
"TANDEM_BASE_URL",
"TANDEM_CLIENT_ID",
"TANDEM_CLIENT_SECRET",
"TASKER_ENABLE_WEBHOOKS",
"TEAM_ID_VERCEL",
"TELEMETRY_DEBUG",
"TWILIO_MESSAGING_SID",
"TWILIO_OPT_OUT_ENABLED",
"TWILIO_PHONE_NUMBER",
"TWILIO_WHATSAPP_PHONE_NUMBER",
"TWILIO_SID",
"TWILIO_TOKEN",
"TWILIO_VERIFY_SID",
"TWILIO_WHATSAPP_REMINDER_CONTENT_SID",
"TWILIO_WHATSAPP_CANCELLED_CONTENT_SID",
"TWILIO_WHATSAPP_RESCHEDULED_CONTENT_SID",
"TWILIO_WHATSAPP_COMPLETED_CONTENT_SID",
"UPSTASH_REDIS_REST_TOKEN",
"UPSTASH_REDIS_REST_URL",
"UNKEY_ROOT_KEY",
"USERNAME_BLACKLIST_URL",
"VERCEL_ENV",
"VERCEL_URL",
"VITAL_API_KEY",
"VITAL_DEVELOPMENT_MODE",
"VITAL_REGION",
"VITAL_WEBHOOK_SECRET",
"ZAPIER_INVITE_LINK",
"ZOHOCRM_CLIENT_ID",
"ZOHOCRM_CLIENT_SECRET",
"ZOOM_CLIENT_ID",
"ZOOM_CLIENT_SECRET",
"REVERT_API_KEY",
"REVERT_API_URL",
"REVERT_PUBLIC_TOKEN",
"RESEND_API_KEY",
"LOCAL_TESTING_DOMAIN_VERCEL",
"AUTH_BEARER_TOKEN_CLOUDFLARE",
"CLOUDFLARE_ZONE_ID",
"NEXT_PUBLIC_MINTLIFY_CHAT_API_KEY",
"NEXT_PUBLIC_CHAT_API_URL",
"NEXT_PUBLIC_DOCS_URL",
"CLOUDFLARE_VERCEL_CNAME",
"CLOUDFLARE_DNS",
"EMBED_PUBLIC_EMBED_FINGER_PRINT",
"EMBED_PUBLIC_EMBED_VERSION",
"EMBED_PUBLIC_WEBAPP_URL",
"EMBED_PUBLIC_VERCEL_URL",
"EMBED_PUBLIC_EMBED_LIB_URL",
"NEXT_PUBLIC_ENABLE_PROFILE_SWITCHER",
"NEXT_PUBLIC_QUERY_AVAILABLE_SLOTS_INTERVAL_SECONDS",
"NEXT_PUBLIC_QUERY_RESERVATION_INTERVAL_SECONDS",
"NEXT_PUBLIC_QUERY_RESERVATION_STALE_TIME_SECONDS",
"NEXT_PUBLIC_INVALIDATE_AVAILABLE_SLOTS_ON_BOOKING_FORM",
"NEXT_PUBLIC_QUICK_AVAILABILITY_ROLLOUT",
"NEXT_PUBLIC_HEAD_SCRIPTS",
"NEXT_PUBLIC_BODY_SCRIPTS",
"NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED",
"NEXT_PUBLIC_API_V2_ROOT_URL",
"NEXT_PUBLIC_VAPID_PUBLIC_KEY",
"VAPID_PRIVATE_KEY",
"CAL_VIDEO_BUCKET_NAME",
"CAL_VIDEO_BUCKET_REGION",
"CAL_VIDEO_ASSUME_ROLE_ARN",
"CAL_VIDEO_MEETING_LINK_FOR_TESTING",
"NEXT_PUBLIC_POSTHOG_KEY",
"NEXT_PUBLIC_POSTHOG_HOST",
"VAPID_PRIVATE_KEY",
"HUDDLE01_API_TOKEN",
"LINGO_DOT_DEV_API_KEY",
"DIRECTORY_IDS_TO_LOG",
"NEXT_PUBLIC_SINGLE_ORG_SLUG",
"GOOGLE_REFRESH_TOKEN",
"GOOGLE_CLIENT_ID",
"GOOGLE_CLIENT_SECRET",
"ZOOM_REFRESH_TOKEN",
"CALCOM_ADMIN_API_KEY",
"NEXT_PUBLIC_SINGLE_ORG_MODE_ENABLED",
"CALCOM_SERVICE_ACCOUNT_ENCRYPTION_KEY",
"CAL_VIDEO_RECORDING_TOKEN_SECRET",
"ORGANIZER_EMAIL_EXEMPT_DOMAINS",
"SLOTS_CACHE_TTL"
]
} }
+34 -34
View File
@@ -2500,7 +2500,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.312" "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.314"
"@calcom/platform-types": "*" "@calcom/platform-types": "*"
"@calcom/platform-utils": "*" "@calcom/platform-utils": "*"
"@calcom/prisma": "*" "@calcom/prisma": "*"
@@ -3559,13 +3559,13 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.312": "@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.314":
version: 0.0.312 version: 0.0.314
resolution: "@calcom/platform-libraries@npm:0.0.312" resolution: "@calcom/platform-libraries@npm:0.0.314"
dependencies: dependencies:
"@calcom/features": "*" "@calcom/features": "*"
"@calcom/lib": "*" "@calcom/lib": "*"
checksum: 46520737404a61f3aa96c6f1adfa81e67f645fd984f495662ca7c63184b0310117a1dad57ddbe7ea487f2e5a0c9cbe62749a9a43b3197a021b19e3ccadc1f5b2 checksum: b59eb591273334ed7d7d07a8f0518eb8cb40255883dff49626f85d3bd804db4da357bf77cd4f7b14ae0743d0a699647232b39956af9123bdc5e7b2458cab30eb
languageName: node languageName: node
linkType: hard linkType: hard
@@ -21694,7 +21694,7 @@ __metadata:
prismock: ^1.33.4 prismock: ^1.33.4
resize-observer-polyfill: ^1.5.1 resize-observer-polyfill: ^1.5.1
tsc-absolute: ^1.0.0 tsc-absolute: ^1.0.0
turbo: ^1.10.1 turbo: ^2.5.5
typescript: 5.9.0-beta typescript: 5.9.0-beta
vitest: ^2.1.1 vitest: ^2.1.1
vitest-fetch-mock: ^0.3.0 vitest-fetch-mock: ^0.3.0
@@ -44926,58 +44926,58 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-darwin-64@npm:1.10.1": "turbo-darwin-64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-darwin-64@npm:1.10.1" resolution: "turbo-darwin-64@npm:2.5.5"
conditions: os=darwin & cpu=x64 conditions: os=darwin & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-darwin-arm64@npm:1.10.1": "turbo-darwin-arm64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-darwin-arm64@npm:1.10.1" resolution: "turbo-darwin-arm64@npm:2.5.5"
conditions: os=darwin & cpu=arm64 conditions: os=darwin & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-linux-64@npm:1.10.1": "turbo-linux-64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-linux-64@npm:1.10.1" resolution: "turbo-linux-64@npm:2.5.5"
conditions: os=linux & cpu=x64 conditions: os=linux & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-linux-arm64@npm:1.10.1": "turbo-linux-arm64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-linux-arm64@npm:1.10.1" resolution: "turbo-linux-arm64@npm:2.5.5"
conditions: os=linux & cpu=arm64 conditions: os=linux & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-windows-64@npm:1.10.1": "turbo-windows-64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-windows-64@npm:1.10.1" resolution: "turbo-windows-64@npm:2.5.5"
conditions: os=win32 & cpu=x64 conditions: os=win32 & cpu=x64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo-windows-arm64@npm:1.10.1": "turbo-windows-arm64@npm:2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo-windows-arm64@npm:1.10.1" resolution: "turbo-windows-arm64@npm:2.5.5"
conditions: os=win32 & cpu=arm64 conditions: os=win32 & cpu=arm64
languageName: node languageName: node
linkType: hard linkType: hard
"turbo@npm:^1.10.1": "turbo@npm:^2.5.5":
version: 1.10.1 version: 2.5.5
resolution: "turbo@npm:1.10.1" resolution: "turbo@npm:2.5.5"
dependencies: dependencies:
turbo-darwin-64: 1.10.1 turbo-darwin-64: 2.5.5
turbo-darwin-arm64: 1.10.1 turbo-darwin-arm64: 2.5.5
turbo-linux-64: 1.10.1 turbo-linux-64: 2.5.5
turbo-linux-arm64: 1.10.1 turbo-linux-arm64: 2.5.5
turbo-windows-64: 1.10.1 turbo-windows-64: 2.5.5
turbo-windows-arm64: 1.10.1 turbo-windows-arm64: 2.5.5
dependenciesMeta: dependenciesMeta:
turbo-darwin-64: turbo-darwin-64:
optional: true optional: true
@@ -44993,7 +44993,7 @@ __metadata:
optional: true optional: true
bin: bin:
turbo: bin/turbo turbo: bin/turbo
checksum: 3551c57be67e833583de549d74d5ec991e0f1e38047e25600b8acc085a1a8a4007992259a015eb9f2c0abd68156545800bd34e7b5ad337d1998b32461952d3fb checksum: 8da6525097ba17bb59571eb512f216362b046bbf88765ddb05fb9937a44e04eeba3bb883c55fca53a7e8a104e02366b4f6f99ef90d12078d0dc95d4659004af6
languageName: node languageName: node
linkType: hard linkType: hard