refactor: platform managed user org admin access (#22597)
* refactor: examples app setup * wip: view org event types as org administrator * fix: create event type has org admin * fix: create event type has org admin * fix: allow org admin team event type access * fix: uncaught error * chore: bump platform libs * refactor: getting event type for org admin * add test * chore: libraries
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.267",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.268",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.s
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
@@ -21,6 +22,7 @@ import { Module } from "@nestjs/common";
|
||||
@Module({
|
||||
imports: [PrismaModule, EventTypesModule_2024_06_14, OrganizationsModule, TeamsEventTypesModule],
|
||||
providers: [
|
||||
OrganizationsTeamsRepository,
|
||||
EventTypesAtomService,
|
||||
ConferencingAtomsService,
|
||||
AttributesAtomsService,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { systemBeforeFieldEmail } from "@/ee/event-types/event-types_2024_06_14/
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service";
|
||||
@@ -53,7 +54,8 @@ export class EventTypesAtomService {
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly eventTypeService: EventTypesService_2024_06_14,
|
||||
private readonly teamEventTypeService: TeamsEventTypesService
|
||||
private readonly teamEventTypeService: TeamsEventTypesService,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository
|
||||
) {}
|
||||
|
||||
private async getTeamSlug(teamId: number): Promise<string> {
|
||||
@@ -88,10 +90,12 @@ export class EventTypesAtomService {
|
||||
throw new NotFoundException(`Event type with id ${eventTypeId} not found`);
|
||||
}
|
||||
|
||||
if (eventType?.team?.id) {
|
||||
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
|
||||
} else {
|
||||
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
if (!isUserOrganizationAdmin) {
|
||||
if (eventType?.team?.id) {
|
||||
await this.checkTeamOwnsEventType(user.id, eventType.eventType.id, eventType.team.id);
|
||||
} else {
|
||||
this.eventTypeService.checkUserOwnsEventType(user.id, eventType.eventType);
|
||||
}
|
||||
}
|
||||
|
||||
// note (Lauris): don't show platform owner as one of the people that can be assigned to managed team event type
|
||||
@@ -115,7 +119,8 @@ export class EventTypesAtomService {
|
||||
user: UserWithProfile,
|
||||
teamId: number
|
||||
) {
|
||||
await this.checkCanUpdateTeamEventType(user.id, eventTypeId, teamId, body.scheduleId);
|
||||
await this.checkCanUpdateTeamEventType(user, eventTypeId, teamId, body.scheduleId);
|
||||
|
||||
const eventTypeUser = await this.eventTypeService.getUserToUpdateEvent(user);
|
||||
const bookingFields = body.bookingFields ? [...body.bookingFields] : undefined;
|
||||
|
||||
@@ -175,14 +180,31 @@ export class EventTypesAtomService {
|
||||
}
|
||||
|
||||
async checkCanUpdateTeamEventType(
|
||||
userId: number,
|
||||
user: UserWithProfile,
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
scheduleId: number | null | undefined
|
||||
) {
|
||||
await this.checkTeamOwnsEventType(userId, eventTypeId, teamId);
|
||||
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||
|
||||
if (organizationId) {
|
||||
const isUserOrganizationAdmin = await this.membershipsRepository.isUserOrganizationAdmin(
|
||||
user.id,
|
||||
organizationId
|
||||
);
|
||||
|
||||
if (isUserOrganizationAdmin) {
|
||||
const orgTeam = await this.organizationsTeamsRepository.findOrgTeam(organizationId, teamId);
|
||||
if (orgTeam) {
|
||||
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.checkTeamOwnsEventType(user.id, eventTypeId, teamId);
|
||||
await this.teamEventTypeService.validateEventTypeExists(teamId, eventTypeId);
|
||||
await this.eventTypeService.checkUserOwnsSchedule(userId, scheduleId);
|
||||
await this.eventTypeService.checkUserOwnsSchedule(user.id, scheduleId);
|
||||
}
|
||||
|
||||
async checkTeamOwnsEventType(userId: number, eventTypeId: number, teamId: number) {
|
||||
|
||||
+11
@@ -27,6 +27,17 @@ export class OrganizationsMembershipService {
|
||||
return this.organizationsMembershipOutputService.getOrgMembershipOutput(membership);
|
||||
}
|
||||
|
||||
async isOrgAdminOrOwner(organizationId: number, userId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId(
|
||||
organizationId,
|
||||
userId
|
||||
);
|
||||
if (!membership) {
|
||||
return false;
|
||||
}
|
||||
return membership.role === "ADMIN" || membership.role === "OWNER";
|
||||
}
|
||||
|
||||
async getOrgMembershipByUserId(organizationId: number, userId: number) {
|
||||
const membership = await this.organizationsMembershipRepository.findOrgMembershipByUserId(
|
||||
organizationId,
|
||||
|
||||
+10
-7
@@ -14,6 +14,7 @@ import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-a
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard";
|
||||
import { OrganizationsMembershipService } from "@/modules/organizations/memberships/services/organizations-membership.service";
|
||||
import { CreateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/create-organization-team.input";
|
||||
import { UpdateOrgTeamDto } from "@/modules/organizations/teams/index/inputs/update-organization-team.input";
|
||||
import {
|
||||
@@ -56,7 +57,10 @@ import { Team } from "@calcom/prisma/client";
|
||||
@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER)
|
||||
@ApiHeader(OPTIONAL_API_KEY_HEADER)
|
||||
export class OrganizationsTeamsController {
|
||||
constructor(private organizationsTeamsService: OrganizationsTeamsService) {}
|
||||
constructor(
|
||||
private organizationsTeamsService: OrganizationsTeamsService,
|
||||
private organizationsMembershipService: OrganizationsMembershipService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: "Get all teams" })
|
||||
@@ -84,12 +88,11 @@ export class OrganizationsTeamsController {
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<OrgMeTeamsOutputResponseDto> {
|
||||
const { skip, take } = queryParams;
|
||||
const teams = await this.organizationsTeamsService.getPaginatedOrgUserTeams(
|
||||
orgId,
|
||||
user.id,
|
||||
skip ?? 0,
|
||||
take ?? 250
|
||||
);
|
||||
const isOrgAdminOrOwner = await this.organizationsMembershipService.isOrgAdminOrOwner(orgId, user.id);
|
||||
const teams = isOrgAdminOrOwner
|
||||
? await this.organizationsTeamsService.getPaginatedOrgTeamsWithMembers(orgId, skip ?? 0, take ?? 250)
|
||||
: await this.organizationsTeamsService.getPaginatedOrgUserTeams(orgId, user.id, skip ?? 0, take ?? 250);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: teams.map((team) => {
|
||||
|
||||
@@ -107,4 +107,17 @@ export class OrganizationsTeamsRepository {
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgTeamsPaginatedWithMembers(organizationId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
parentId: organizationId,
|
||||
},
|
||||
include: {
|
||||
members: { select: { accepted: true, userId: true, role: true } },
|
||||
},
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -24,6 +24,15 @@ export class OrganizationsTeamsService {
|
||||
return teams;
|
||||
}
|
||||
|
||||
async getPaginatedOrgTeamsWithMembers(organizationId: number, skip = 0, take = 250) {
|
||||
const teams = await this.organizationsTeamRepository.findOrgTeamsPaginatedWithMembers(
|
||||
organizationId,
|
||||
skip,
|
||||
take
|
||||
);
|
||||
return teams;
|
||||
}
|
||||
|
||||
async getPaginatedOrgTeams(organizationId: number, skip = 0, take = 250) {
|
||||
const teams = await this.organizationsTeamRepository.findOrgTeamsPaginated(organizationId, skip, take);
|
||||
return teams;
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import prismock from "../../../tests/libs/__mocks__/prisma";
|
||||
|
||||
import { mockNoTranslations } from "@calcom/web/test/utils/bookingScenario/bookingScenario";
|
||||
|
||||
import { describe, test, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
import { getRawEventType } from "./getEventTypeById";
|
||||
|
||||
vi.mock("@calcom/lib/server/i18n", () => ({
|
||||
getTranslation: (key: string) => () => key,
|
||||
}));
|
||||
|
||||
describe("getRawEventType", () => {
|
||||
beforeEach(() => {
|
||||
mockNoTranslations();
|
||||
});
|
||||
|
||||
describe("Regular user access", () => {
|
||||
test("should fetch event type when user owns it", async () => {
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
username: "testuser",
|
||||
email: "testuser@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Test Event Type",
|
||||
slug: "test-event",
|
||||
length: 30,
|
||||
userId: user.id,
|
||||
users: {
|
||||
connect: [{ id: user.id }],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: false,
|
||||
currentOrganizationId: null,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(eventType.id);
|
||||
expect(result?.title).toBe("Test Event Type");
|
||||
expect(result?.userId).toBe(user.id);
|
||||
});
|
||||
|
||||
test.skip("should return null when user doesn't have access to event type", async () => {
|
||||
// note(Lauris): test skipped because somehow when creating event type eventType.users includes otherUser
|
||||
const owner = await prismock.user.create({
|
||||
data: {
|
||||
username: "owner",
|
||||
email: "owner1@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const otherUser = await prismock.user.create({
|
||||
data: {
|
||||
username: "otheruser",
|
||||
email: "otheruser@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Owner's Event Type",
|
||||
slug: "owner-event",
|
||||
length: 30,
|
||||
userId: owner.id,
|
||||
users: {
|
||||
connect: [{ id: owner.id }],
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
|
||||
await prismock.user.update({
|
||||
where: {
|
||||
id: otherUser.id,
|
||||
},
|
||||
data: {
|
||||
eventTypes: {
|
||||
disconnect: [{ id: eventType.id }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: otherUser.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: false,
|
||||
currentOrganizationId: null,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Organization admin access", () => {
|
||||
test("should fetch team event type when user is org admin", async () => {
|
||||
const organization = await prismock.team.create({
|
||||
data: {
|
||||
id: 100,
|
||||
name: "Test Organization",
|
||||
slug: "test-org",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const team = await prismock.team.create({
|
||||
data: {
|
||||
id: 200,
|
||||
name: "Test Team",
|
||||
slug: "test-team",
|
||||
parentId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const orgAdmin = await prismock.user.create({
|
||||
data: {
|
||||
username: "orgadmin",
|
||||
email: "orgadmin@example.com",
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Team Event Type",
|
||||
slug: "team-event",
|
||||
length: 30,
|
||||
teamId: team.id,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: orgAdmin.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: true,
|
||||
currentOrganizationId: organization.id,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(eventType.id);
|
||||
expect(result?.title).toBe("Team Event Type");
|
||||
expect(result?.teamId).toBe(team.id);
|
||||
});
|
||||
|
||||
test("should fetch user event type when user is org admin and user belongs to org", async () => {
|
||||
const organization = await prismock.team.create({
|
||||
data: {
|
||||
id: 101,
|
||||
name: "Test Organization 2",
|
||||
slug: "test-org-2",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgAdmin = await prismock.user.create({
|
||||
data: {
|
||||
username: "orgadmin2",
|
||||
email: "orgadmin2@example.com",
|
||||
organizationId: organization.id,
|
||||
},
|
||||
});
|
||||
|
||||
const orgUser = await prismock.user.create({
|
||||
data: {
|
||||
username: "orguser",
|
||||
email: "orguser@example.com",
|
||||
organizationId: organization.id,
|
||||
profiles: {
|
||||
create: {
|
||||
organizationId: organization.id,
|
||||
uid: "orguser",
|
||||
username: "orguser",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Org User Event Type",
|
||||
slug: "org-user-event",
|
||||
length: 30,
|
||||
userId: orgUser.id,
|
||||
users: {
|
||||
connect: [{ id: orgUser.id }],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
users: true,
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: orgAdmin.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: true,
|
||||
currentOrganizationId: organization.id,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(eventType.id);
|
||||
expect(result?.title).toBe("Org User Event Type");
|
||||
expect(result?.userId).toBe(orgUser.id);
|
||||
});
|
||||
|
||||
test("should return null when org admin tries to access event type from different org", async () => {
|
||||
const org1 = await prismock.team.create({
|
||||
data: {
|
||||
id: 102,
|
||||
name: "Organization 1",
|
||||
slug: "org-1",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const org2 = await prismock.team.create({
|
||||
data: {
|
||||
id: 103,
|
||||
name: "Organization 2",
|
||||
slug: "org-2",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const team1 = await prismock.team.create({
|
||||
data: {
|
||||
id: 201,
|
||||
name: "Team in Org 1",
|
||||
slug: "team-org-1",
|
||||
parentId: org1.id,
|
||||
},
|
||||
});
|
||||
|
||||
const org2Admin = await prismock.user.create({
|
||||
data: {
|
||||
username: "org2admin",
|
||||
email: "org2admin@example.com",
|
||||
organizationId: org2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Org 1 Team Event",
|
||||
slug: "org1-team-event",
|
||||
length: 30,
|
||||
teamId: team1.id,
|
||||
},
|
||||
include: {
|
||||
team: true,
|
||||
users: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: org2Admin.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: true,
|
||||
currentOrganizationId: org2.id,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test("should fallback to regular user access when org admin flag is true but no organizationId", async () => {
|
||||
const user = await prismock.user.create({
|
||||
data: {
|
||||
username: "regularuser",
|
||||
email: "regularuser@example.com",
|
||||
},
|
||||
});
|
||||
|
||||
const eventType = await prismock.eventType.create({
|
||||
data: {
|
||||
title: "Regular User Event",
|
||||
slug: "regular-user-event",
|
||||
length: 30,
|
||||
userId: user.id,
|
||||
users: {
|
||||
connect: [{ id: user.id }],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
users: true,
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await getRawEventType({
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
isUserOrganizationAdmin: true,
|
||||
currentOrganizationId: null,
|
||||
prisma: prismock as any,
|
||||
});
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe(eventType.id);
|
||||
expect(result?.userId).toBe(user.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,8 +53,13 @@ export const getEventTypeById = async ({
|
||||
timeZone: true,
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
const eventTypeRepo = new EventTypeRepository(prisma);
|
||||
const rawEventType = await eventTypeRepo.findById({ id: eventTypeId, userId });
|
||||
const rawEventType = await getRawEventType({
|
||||
userId,
|
||||
eventTypeId,
|
||||
isUserOrganizationAdmin,
|
||||
currentOrganizationId,
|
||||
prisma,
|
||||
});
|
||||
|
||||
if (!rawEventType) {
|
||||
if (isTrpcCall) {
|
||||
@@ -260,4 +265,26 @@ export const getEventTypeById = async ({
|
||||
return finalObj;
|
||||
};
|
||||
|
||||
export async function getRawEventType({
|
||||
userId,
|
||||
eventTypeId,
|
||||
isUserOrganizationAdmin,
|
||||
currentOrganizationId,
|
||||
prisma,
|
||||
}: Omit<getEventTypeByIdProps, "isTrpcCall">) {
|
||||
const eventTypeRepo = new EventTypeRepository(prisma);
|
||||
|
||||
if (isUserOrganizationAdmin && currentOrganizationId) {
|
||||
return await eventTypeRepo.findByIdForOrgAdmin({
|
||||
id: eventTypeId,
|
||||
organizationId: currentOrganizationId,
|
||||
});
|
||||
}
|
||||
|
||||
return await eventTypeRepo.findById({
|
||||
id: eventTypeId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
export default getEventTypeById;
|
||||
|
||||
@@ -799,6 +799,282 @@ export class EventTypeRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdForOrgAdmin({ id, organizationId }: { id: number; organizationId: number }) {
|
||||
const userSelect = {
|
||||
name: true,
|
||||
avatarUrl: true,
|
||||
username: true,
|
||||
id: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
defaultScheduleId: true,
|
||||
isPlatformManaged: true,
|
||||
timeZone: true,
|
||||
} satisfies Prisma.UserSelect;
|
||||
|
||||
const CompleteEventTypeSelect = {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
interfaceLanguage: true,
|
||||
length: true,
|
||||
isInstantEvent: true,
|
||||
instantMeetingExpiryTimeOffsetInSeconds: true,
|
||||
instantMeetingParameters: true,
|
||||
aiPhoneCallConfig: true,
|
||||
offsetStart: true,
|
||||
hidden: true,
|
||||
locations: true,
|
||||
eventName: true,
|
||||
customInputs: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
metadata: true,
|
||||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
requiresConfirmation: true,
|
||||
requiresConfirmationForFreeEmail: true,
|
||||
canSendCalVideoTranscriptionEmails: true,
|
||||
requiresConfirmationWillBlockSlot: true,
|
||||
requiresBookerEmailVerification: true,
|
||||
autoTranslateDescriptionEnabled: true,
|
||||
fieldTranslations: {
|
||||
select: {
|
||||
translatedText: true,
|
||||
targetLocale: true,
|
||||
field: true,
|
||||
},
|
||||
},
|
||||
recurringEvent: true,
|
||||
hideCalendarNotes: true,
|
||||
hideCalendarEventDetails: true,
|
||||
disableGuests: true,
|
||||
disableCancelling: true,
|
||||
disableRescheduling: true,
|
||||
allowReschedulingCancelledBookings: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
slotInterval: true,
|
||||
hashedLink: hashedLinkSelect,
|
||||
eventTypeColor: true,
|
||||
bookingLimits: true,
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
durationLimits: true,
|
||||
maxActiveBookingsPerBooker: true,
|
||||
maxActiveBookingPerBookerOfferReschedule: true,
|
||||
assignAllTeamMembers: true,
|
||||
allowReschedulingPastBookings: true,
|
||||
hideOrganizerEmail: true,
|
||||
assignRRMembersUsingSegment: true,
|
||||
rrSegmentQueryValue: true,
|
||||
isRRWeightsEnabled: true,
|
||||
rescheduleWithSameRoundRobinHost: true,
|
||||
successRedirectUrl: true,
|
||||
forwardParamsSuccessRedirect: true,
|
||||
currency: true,
|
||||
bookingFields: true,
|
||||
useEventTypeDestinationCalendarEmail: true,
|
||||
customReplyToEmail: true,
|
||||
owner: {
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
slug: true,
|
||||
parentId: true,
|
||||
rrTimestampBasis: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
organizationSettings: {
|
||||
select: {
|
||||
lockEventTypeCreationForUsers: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
members: {
|
||||
select: {
|
||||
role: true,
|
||||
accepted: true,
|
||||
user: {
|
||||
select: {
|
||||
...userSelect,
|
||||
eventTypes: {
|
||||
select: {
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
restrictionScheduleId: true,
|
||||
useBookerTimezone: true,
|
||||
users: {
|
||||
select: userSelect,
|
||||
},
|
||||
schedulingType: true,
|
||||
schedule: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
instantMeetingSchedule: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
restrictionSchedule: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
hosts: {
|
||||
select: {
|
||||
isFixed: true,
|
||||
userId: true,
|
||||
priority: true,
|
||||
weight: true,
|
||||
scheduleId: true,
|
||||
user: {
|
||||
select: {
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
userId: true,
|
||||
price: true,
|
||||
children: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
avatarUrl: true,
|
||||
name: true,
|
||||
username: true,
|
||||
email: true,
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
hidden: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
destinationCalendar: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
webhooks: {
|
||||
select: {
|
||||
id: true,
|
||||
subscriberUrl: true,
|
||||
payloadTemplate: true,
|
||||
active: true,
|
||||
eventTriggers: true,
|
||||
secret: true,
|
||||
eventTypeId: true,
|
||||
},
|
||||
},
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
select: {
|
||||
name: true,
|
||||
id: true,
|
||||
trigger: true,
|
||||
time: true,
|
||||
timeUnit: true,
|
||||
userId: true,
|
||||
teamId: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
members: true,
|
||||
},
|
||||
},
|
||||
activeOn: {
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
parentId: true,
|
||||
_count: {
|
||||
select: {
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
secondaryEmailId: true,
|
||||
maxLeadThreshold: true,
|
||||
includeNoShowInRRCalculation: true,
|
||||
useEventLevelSelectedCalendars: true,
|
||||
calVideoSettings: {
|
||||
select: {
|
||||
disableRecordingForGuests: true,
|
||||
disableRecordingForOrganizer: true,
|
||||
enableAutomaticTranscription: true,
|
||||
enableAutomaticRecordingForOrganizer: true,
|
||||
disableTranscriptionForGuests: true,
|
||||
disableTranscriptionForOrganizer: true,
|
||||
redirectUrlOnExit: true,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.EventTypeSelect;
|
||||
|
||||
const orgUserEventTypeQuery = {
|
||||
AND: [{ userId: { not: null } }, { owner: { profiles: { some: { organizationId } } } }],
|
||||
};
|
||||
const orgTeamEventTypeQuery = {
|
||||
AND: [{ teamId: { not: null } }, { team: { parentId: organizationId } }],
|
||||
};
|
||||
|
||||
return await this.prismaClient.eventType.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ id },
|
||||
{
|
||||
OR: [orgUserEventTypeQuery, orgTeamEventTypeQuery],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: CompleteEventTypeSelect,
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdMinimal({ id }: { id: number }) {
|
||||
return await this.prismaClient.eventType.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -13,8 +13,8 @@ import "@calcom/atoms/globals.min.css";
|
||||
const poppins = Poppins({ subsets: ["latin"], weight: ["400", "800"] });
|
||||
type TUser = Data["users"][0];
|
||||
|
||||
function generateRandomEmail() {
|
||||
const localPartLength = 10;
|
||||
function generateRandomEmail(name: string) {
|
||||
const localPartLength = 5;
|
||||
const domain = ["example.com", "example.net", "example.org"];
|
||||
|
||||
const randomLocalPart = Array.from({ length: localPartLength }, () =>
|
||||
@@ -23,7 +23,7 @@ function generateRandomEmail() {
|
||||
|
||||
const randomDomain = domain[Math.floor(Math.random() * domain.length)];
|
||||
|
||||
return `${randomLocalPart}@${randomDomain}`;
|
||||
return `${name}-${randomLocalPart}@${randomDomain}`;
|
||||
}
|
||||
|
||||
// note(Lauris): needed because useEffect kicks in twice creating 2 parallel requests
|
||||
@@ -50,11 +50,11 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const randomEmailOne = generateRandomEmail();
|
||||
const randomEmailTwo = generateRandomEmail();
|
||||
const randomEmailThree = generateRandomEmail();
|
||||
const randomEmailFour = generateRandomEmail();
|
||||
const randomEmailFive = generateRandomEmail();
|
||||
const randomEmailOne = generateRandomEmail("keith");
|
||||
const randomEmailTwo = generateRandomEmail("somay");
|
||||
const randomEmailThree = generateRandomEmail("rajiv");
|
||||
const randomEmailFour = generateRandomEmail("morgan");
|
||||
const randomEmailFive = generateRandomEmail("lauris");
|
||||
|
||||
if (!seeding) {
|
||||
seeding = true;
|
||||
|
||||
@@ -12,6 +12,49 @@ type Data = {
|
||||
accessToken: string;
|
||||
};
|
||||
|
||||
async function createUserWithDefaultSchedule(email: string, name: string, avatarUrl: string) {
|
||||
const localUser = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
const managedUserResponse = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
name,
|
||||
avatarUrl,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const managedUserResponseBody = await managedUserResponse.json();
|
||||
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (managedUserResponseBody.data?.refreshToken as string) ?? "",
|
||||
accessToken: (managedUserResponseBody.data?.accessToken as string) ?? "",
|
||||
calcomUserId: managedUserResponseBody.data?.user.id,
|
||||
calcomUsername: (managedUserResponseBody.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUser.id },
|
||||
});
|
||||
|
||||
await createDefaultSchedule(managedUserResponseBody.data?.accessToken as string);
|
||||
|
||||
return managedUserResponseBody.data;
|
||||
}
|
||||
|
||||
// example endpoint to create a managed cal.com user
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||
const { emails } = JSON.parse(req.body);
|
||||
@@ -31,195 +74,31 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
});
|
||||
}
|
||||
|
||||
const localUserOne = await prisma.user.create({
|
||||
data: {
|
||||
email: emailOne,
|
||||
},
|
||||
});
|
||||
|
||||
const localUserTwo = await prisma.user.create({
|
||||
data: {
|
||||
email: emailTwo,
|
||||
},
|
||||
});
|
||||
|
||||
const localUserThree = await prisma.user.create({
|
||||
data: {
|
||||
email: emailThree,
|
||||
},
|
||||
});
|
||||
|
||||
const localUserFour = await prisma.user.create({
|
||||
data: {
|
||||
email: emailFour,
|
||||
},
|
||||
});
|
||||
|
||||
const localUserFive = await prisma.user.create({
|
||||
data: {
|
||||
email: emailFive,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailOne,
|
||||
name: "John Jones",
|
||||
avatarUrl:
|
||||
"https://images.unsplash.com/photo-1527980965255-d3b416303d12?q=80&w=3023&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
}),
|
||||
}
|
||||
const managedUserResponseOne = await createUserWithDefaultSchedule(
|
||||
emailOne,
|
||||
"Keith",
|
||||
"https://images.unsplash.com/photo-1527980965255-d3b416303d12?q=80&w=3023&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
);
|
||||
|
||||
const body = await response.json();
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (body.data?.refreshToken as string) ?? "",
|
||||
accessToken: (body.data?.accessToken as string) ?? "",
|
||||
calcomUserId: body.data?.user.id,
|
||||
calcomUsername: (body.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserOne.id },
|
||||
});
|
||||
|
||||
const responseTwo = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailTwo,
|
||||
name: "Jane Doe",
|
||||
avatarUrl:
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
}),
|
||||
}
|
||||
const managedUserResponseTwo = await createUserWithDefaultSchedule(
|
||||
emailTwo,
|
||||
"Somay",
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
);
|
||||
const bodyTwo = await responseTwo.json();
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (bodyTwo.data?.refreshToken as string) ?? "",
|
||||
accessToken: (bodyTwo.data?.accessToken as string) ?? "",
|
||||
calcomUserId: bodyTwo.data?.user.id,
|
||||
calcomUsername: (bodyTwo.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserTwo.id },
|
||||
});
|
||||
|
||||
const responseThree = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailThree,
|
||||
name: "Rajiv",
|
||||
avatarUrl:
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
}),
|
||||
}
|
||||
const managedUserResponseThree = await createUserWithDefaultSchedule(
|
||||
emailThree,
|
||||
"Rajiv",
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
);
|
||||
const bodyThree = await responseThree.json();
|
||||
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (bodyThree.data?.refreshToken as string) ?? "",
|
||||
accessToken: (bodyThree.data?.accessToken as string) ?? "",
|
||||
calcomUserId: bodyThree.data?.user.id,
|
||||
calcomUsername: (bodyThree.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserThree.id },
|
||||
});
|
||||
|
||||
const responseFour = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailFour,
|
||||
name: "Morgan",
|
||||
avatarUrl:
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
}),
|
||||
}
|
||||
const managedUserResponseFour = await createUserWithDefaultSchedule(
|
||||
emailFour,
|
||||
"Morgan",
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
);
|
||||
const bodyFour = await responseFour.json();
|
||||
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (bodyFour.data?.refreshToken as string) ?? "",
|
||||
accessToken: (bodyFour.data?.accessToken as string) ?? "",
|
||||
calcomUserId: bodyFour.data?.user.id,
|
||||
calcomUsername: (bodyFour.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserFour.id },
|
||||
});
|
||||
|
||||
const responseFive = await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/oauth-clients/${process.env.NEXT_PUBLIC_X_CAL_ID}/users`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: emailFive,
|
||||
name: "Lauris",
|
||||
avatarUrl:
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
|
||||
}),
|
||||
}
|
||||
const managedUserResponseFive = await createUserWithDefaultSchedule(
|
||||
emailFive,
|
||||
"Lauris",
|
||||
"https://plus.unsplash.com/premium_photo-1668319915476-5cc7717e00f1?q=80&w=3164&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||
);
|
||||
const bodyFive = await responseFive.json();
|
||||
|
||||
await prisma.user.update({
|
||||
data: {
|
||||
refreshToken: (bodyFive.data?.refreshToken as string) ?? "",
|
||||
accessToken: (bodyFive.data?.accessToken as string) ?? "",
|
||||
calcomUserId: bodyFive.data?.user.id,
|
||||
calcomUsername: (bodyFive.data?.user.username as string) ?? "",
|
||||
},
|
||||
where: { id: localUserFive.id },
|
||||
});
|
||||
|
||||
await createDefaultSchedule(body.data?.accessToken as string);
|
||||
await createDefaultSchedule(bodyTwo.data?.accessToken as string);
|
||||
await createDefaultSchedule(bodyThree.data?.accessToken as string);
|
||||
await createDefaultSchedule(bodyFour.data?.accessToken as string);
|
||||
await createDefaultSchedule(bodyFive.data?.accessToken as string);
|
||||
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
const organizationId = process.env.ORGANIZATION_ID;
|
||||
@@ -227,27 +106,37 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
||||
throw new Error("Organization ID is not set");
|
||||
}
|
||||
|
||||
const team = await createTeam(+organizationId, "Team Doe");
|
||||
const team = await createTeam(+organizationId, `Platform devs - ${Date.now()}`);
|
||||
if (!team) {
|
||||
throw new Error("Failed to create team. Probably your platform team does not have required plan.");
|
||||
}
|
||||
|
||||
await createMembership(+organizationId, team.id, body.data?.user.id);
|
||||
await createMembership(+organizationId, team.id, bodyTwo.data?.user.id);
|
||||
await createMembership(+organizationId, team.id, bodyThree.data?.user.id);
|
||||
await createOrgTeamMembershipMember(+organizationId, team.id, managedUserResponseOne.user.id);
|
||||
await createOrgTeamMembershipMember(+organizationId, team.id, managedUserResponseTwo.user.id);
|
||||
await createOrgTeamMembershipMember(+organizationId, team.id, managedUserResponseThree.user.id);
|
||||
await createOrgTeamMembershipMember(+organizationId, team.id, managedUserResponseFour.user.id);
|
||||
|
||||
await createCollectiveEventType(+organizationId, team.id, [
|
||||
body.data?.user.id,
|
||||
bodyTwo.data?.user.id,
|
||||
bodyThree.data?.user.id,
|
||||
bodyFour.data?.user.id,
|
||||
bodyFive.data?.user.id,
|
||||
managedUserResponseOne.user.id,
|
||||
managedUserResponseTwo.user.id,
|
||||
managedUserResponseThree.user.id,
|
||||
managedUserResponseFour.user.id,
|
||||
]);
|
||||
|
||||
await createRoundRobinEventType(+organizationId, team.id, [
|
||||
managedUserResponseOne.user.id,
|
||||
managedUserResponseTwo.user.id,
|
||||
managedUserResponseThree.user.id,
|
||||
managedUserResponseFour.user.id,
|
||||
]);
|
||||
|
||||
await createOrgMembershipAdmin(+organizationId, managedUserResponseFive.user.id);
|
||||
|
||||
return res.status(200).json({
|
||||
id: body?.data?.user?.id,
|
||||
email: (body.data?.user.email as string) ?? "",
|
||||
username: (body.data?.username as string) ?? "",
|
||||
accessToken: (body.data?.accessToken as string) ?? "",
|
||||
id: managedUserResponseOne?.user?.id,
|
||||
email: (managedUserResponseOne.user.email as string) ?? "",
|
||||
username: (managedUserResponseOne.user.username as string) ?? "",
|
||||
accessToken: (managedUserResponseOne.accessToken as string) ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -276,10 +165,33 @@ async function createTeam(orgId: number, name: string) {
|
||||
return body.data;
|
||||
}
|
||||
|
||||
async function createMembership(orgId: number, teamId: number, userId: number) {
|
||||
async function createOrgTeamMembershipMember(orgId: number, teamId: number, userId: number) {
|
||||
await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams/${teamId}/memberships`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createOrgMembershipAdmin(orgId: number, userId: number) {
|
||||
await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/memberships`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -315,9 +227,34 @@ async function createCollectiveEventType(orgId: number, teamId: number, userIds:
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lengthInMinutes: 60,
|
||||
title: "Doe collective",
|
||||
slug: "doe-collective",
|
||||
schedulingType: "COLLECTIVE",
|
||||
title: "Platform example collective",
|
||||
slug: "platform-example-collective",
|
||||
schedulingType: "collective",
|
||||
hosts: userIds.map((userId) => ({ userId })),
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function createRoundRobinEventType(orgId: number, teamId: number, userIds: number[]) {
|
||||
await fetch(
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
`${process.env.NEXT_PUBLIC_CALCOM_API_URL ?? ""}/organizations/${orgId}/teams/${teamId}/event-types`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_SECRET_KEY]: process.env.X_CAL_SECRET_KEY ?? "",
|
||||
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||
[X_CAL_CLIENT_ID]: process.env.NEXT_PUBLIC_X_CAL_ID ?? "",
|
||||
origin: "http://localhost:4321",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
lengthInMinutes: 60,
|
||||
title: "Platform example round robin",
|
||||
slug: "platform-example-round-robin",
|
||||
schedulingType: "roundRobin",
|
||||
hosts: userIds.map((userId) => ({ userId })),
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -743,6 +743,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
|
||||
onSuccess={(eventType) => {
|
||||
setEventTypeId(null);
|
||||
refetch();
|
||||
refetchTeamEvents();
|
||||
}}
|
||||
onError={(eventType, error) => {
|
||||
console.log(eventType);
|
||||
|
||||
@@ -93,7 +93,8 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
|
||||
if (
|
||||
!isSystemAdmin &&
|
||||
(!hasMembership?.role || !(["ADMIN", "OWNER"].includes(hasMembership.role) || isOrgAdmin))
|
||||
!isOrgAdmin &&
|
||||
(!hasMembership?.role || !["ADMIN", "OWNER"].includes(hasMembership.role))
|
||||
) {
|
||||
console.warn(`User ${userId} does not have permission to create this new event type`);
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
@@ -2519,7 +2519,7 @@ __metadata:
|
||||
"@axiomhq/winston": ^1.2.0
|
||||
"@calcom/platform-constants": "*"
|
||||
"@calcom/platform-enums": "*"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.267"
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.268"
|
||||
"@calcom/platform-types": "*"
|
||||
"@calcom/platform-utils": "*"
|
||||
"@calcom/prisma": "*"
|
||||
@@ -3566,13 +3566,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.267":
|
||||
version: 0.0.267
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.267"
|
||||
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.268":
|
||||
version: 0.0.268
|
||||
resolution: "@calcom/platform-libraries@npm:0.0.268"
|
||||
dependencies:
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
checksum: 0c385514a04ddbd96ab3277774815233801a6eaf9a0d406b473eb7a446602f6f7d98de7f16747893b9fe1d50257db13b050a5726c8e7fe9500750d3ebf7be23a
|
||||
checksum: d361c067dd33e3807c3ba965b3582eadaeec2ef1e2a3114c68e0c2ab1d7149fcca2c72ce00f8f3f341bdf3caeca578898d3345d8615c58da3a66e541e62d2ca5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user