diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 598060279d0aa5..594a9535feeabe 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.305", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts b/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts index c9ce6a9839bcbf..91afc934ab0618 100644 --- a/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts +++ b/apps/api/v2/src/ee/event-types-private-links/controllers/event-types-private-links.controller.ts @@ -69,11 +69,10 @@ export class EventTypesPrivateLinksController { async updatePrivateLink( @Param("eventTypeId", ParseIntPipe) eventTypeId: number, @Param("linkId") linkId: string, - @Body() body: UpdatePrivateLinkBody, - @GetUser("id") userId: number + @Body() body: UpdatePrivateLinkBody ): Promise { const updateInput = { ...body, linkId }; - const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, userId, updateInput); + const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput); return { status: SUCCESS_STATUS, @@ -88,10 +87,9 @@ export class EventTypesPrivateLinksController { @ApiOperation({ summary: "Delete a private link for an event type" }) async deletePrivateLink( @Param("eventTypeId", ParseIntPipe) eventTypeId: number, - @Param("linkId") linkId: string, - @GetUser("id") userId: number + @Param("linkId") linkId: string ): Promise { - await this.privateLinksService.deletePrivateLink(eventTypeId, userId, linkId); + await this.privateLinksService.deletePrivateLink(eventTypeId, linkId); return { status: SUCCESS_STATUS, diff --git a/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts b/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts new file mode 100644 index 00000000000000..e3382f39ad011a --- /dev/null +++ b/apps/api/v2/src/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller.ts @@ -0,0 +1,121 @@ +import { API_VERSIONS_VALUES } from "@/lib/api-versions"; +import { + OPTIONAL_API_KEY_HEADER, + OPTIONAL_X_CAL_CLIENT_ID_HEADER, + OPTIONAL_X_CAL_SECRET_KEY_HEADER, +} from "@/lib/docs/headers"; +import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; +import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; +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 { TeamsEventTypesService } from "@/modules/teams/event-types/services/teams-event-types.service"; +import { Body, Controller, Delete, Get, Param, ParseIntPipe, Patch, Post, UseGuards } from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { + CreatePrivateLinkInput, + CreatePrivateLinkOutput, + DeletePrivateLinkOutput, + GetPrivateLinksOutput, + UpdatePrivateLinkInput, + UpdatePrivateLinkOutput, +} from "@calcom/platform-types"; + +import { PrivateLinksService } from "../services/private-links.service"; + +@Controller({ + path: "/v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links", + version: API_VERSIONS_VALUES, +}) +@DocsTags("Orgs / Teams / Event Types / Private Links") +@ApiHeader(OPTIONAL_X_CAL_CLIENT_ID_HEADER) +@ApiHeader(OPTIONAL_X_CAL_SECRET_KEY_HEADER) +@ApiHeader(OPTIONAL_API_KEY_HEADER) +export class OrganizationsEventTypesPrivateLinksController { + constructor( + private readonly privateLinksService: PrivateLinksService, + private readonly teamsEventTypesService: TeamsEventTypesService + ) {} + + @Post("/") + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) + @ApiOperation({ summary: "Create a private link for a team event type" }) + async createPrivateLink( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, + @Body() body: CreatePrivateLinkInput + ): Promise { + await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); + // Use teamId as the seed for link generation in org/team context + const privateLink = await this.privateLinksService.createPrivateLink(eventTypeId, teamId, body); + return { + status: SUCCESS_STATUS, + data: privateLink, + }; + } + + @Get("/") + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) + @ApiOperation({ summary: "Get all private links for a team event type" }) + async getPrivateLinks( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number + ): Promise { + await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); + const privateLinks = await this.privateLinksService.getPrivateLinks(eventTypeId); + return { + status: SUCCESS_STATUS, + data: privateLinks, + }; + } + + @Patch("/:linkId") + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) + @ApiOperation({ summary: "Update a private link for a team event type" }) + async updatePrivateLink( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, + @Param("linkId") linkId: string, + @Body() body: Omit + ): Promise { + await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); + const updateInput: UpdatePrivateLinkInput = { ...body, linkId }; + const privateLink = await this.privateLinksService.updatePrivateLink(eventTypeId, updateInput); + return { + status: SUCCESS_STATUS, + data: privateLink, + }; + } + + @Delete("/:linkId") + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) + @ApiOperation({ summary: "Delete a private link for a team event type" }) + async deletePrivateLink( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("eventTypeId", ParseIntPipe) eventTypeId: number, + @Param("linkId") linkId: string + ): Promise { + await this.teamsEventTypesService.validateEventTypeExists(teamId, eventTypeId); + await this.privateLinksService.deletePrivateLink(eventTypeId, linkId); + return { + status: SUCCESS_STATUS, + data: { + linkId, + message: "Private link deleted successfully", + }, + }; + } +} diff --git a/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts b/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts index 6bd91072d78e77..c7e9fee623e266 100644 --- a/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts +++ b/apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts @@ -12,7 +12,7 @@ import { PrivateLinksOutputService } from "./services/private-links-output.servi import { PrivateLinksService } from "./services/private-links.service"; @Module({ - imports: [TokensModule, OAuthClientModule, PrismaModule, EventTypesModule_2024_06_14], + imports: [TokensModule, PrismaModule, EventTypesModule_2024_06_14], controllers: [EventTypesPrivateLinksController], providers: [ PrivateLinksService, @@ -21,5 +21,6 @@ import { PrivateLinksService } from "./services/private-links.service"; PrivateLinksRepository, EventTypeOwnershipGuard, ], + exports: [PrivateLinksService], }) export class EventTypesPrivateLinksModule {} diff --git a/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts b/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts index 73a2cf16f550ce..e6161072ebd3bb 100644 --- a/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts +++ b/apps/api/v2/src/ee/event-types-private-links/services/private-links.service.ts @@ -68,11 +68,7 @@ export class PrivateLinksService { } } - async updatePrivateLink( - eventTypeId: number, - userId: number, - input: UpdatePrivateLinkInput - ): Promise { + async updatePrivateLink(eventTypeId: number, input: UpdatePrivateLinkInput): Promise { try { const transformedInput = this.inputService.transformUpdateInput(input); const updatedResult = await this.repo.update(eventTypeId, { @@ -106,7 +102,7 @@ export class PrivateLinksService { } } - async deletePrivateLink(eventTypeId: number, userId: number, linkId: string): Promise { + async deletePrivateLink(eventTypeId: number, linkId: string): Promise { try { const { count } = await this.repo.delete(eventTypeId, linkId); if (count === 0) { diff --git a/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts new file mode 100644 index 00000000000000..838854231c0081 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/event-types/organizations-event-types-private-links.e2e-spec.ts @@ -0,0 +1,183 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { HttpExceptionFilter } from "@/filters/http-exception.filter"; +import { PrismaExceptionFilter } from "@/filters/prisma-exception.filter"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard"; +import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard"; +import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import * as request from "supertest"; +import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { randomString } from "test/utils/randomString"; +import { withApiAuth } from "test/utils/withApiAuth"; + +import { SUCCESS_STATUS } from "@calcom/platform-constants"; +import { CreatePrivateLinkInput } from "@calcom/platform-types"; + +describe("Organizations / Teams / Event Types / Private Links Endpoints", () => { + let app: INestApplication; + + let orgFixture: OrganizationRepositoryFixture; + let teamFixture: TeamRepositoryFixture; + let userFixture: UserRepositoryFixture; + let eventTypesFixture: EventTypesRepositoryFixture; + + let org: any; + let team: any; + let user: any; + let eventType: any; + + const userEmail = `org-private-links-user-${randomString()}@api.com`; + + beforeAll(async () => { + const testingModuleBuilder = withApiAuth( + userEmail, + Test.createTestingModule({ + providers: [PrismaExceptionFilter, HttpExceptionFilter], + imports: [AppModule, UsersModule, TokensModule], + }) + ) + // Bypass org admin plan and admin API checks and roles in this e2e + .overrideGuard(PlatformPlanGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(IsAdminAPIEnabledGuard) + .useValue({ canActivate: () => true }) + .overrideGuard(RolesGuard) + .useValue({ canActivate: () => true }) + // Keep IsOrgGuard and IsTeamInOrg to validate org/team path integrity + .overrideGuard(ApiAuthGuard) + .useValue({ canActivate: () => true }); + + const moduleRef = await testingModuleBuilder.compile(); + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + orgFixture = new OrganizationRepositoryFixture(moduleRef); + teamFixture = new TeamRepositoryFixture(moduleRef); + userFixture = new UserRepositoryFixture(moduleRef); + eventTypesFixture = new EventTypesRepositoryFixture(moduleRef); + + user = await userFixture.create({ + email: userEmail, + username: `org-private-links-user-${randomString()}`, + name: "Test User", + }); + + org = await orgFixture.create({ + name: `org-private-links-org-${randomString()}`, + slug: `org-private-links-org-${randomString()}`, + isOrganization: true, + }); + + team = await teamFixture.create({ + name: `org-private-links-team-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); + + // Create a team-owned event type + eventType = await eventTypesFixture.createTeamEventType({ + title: `org-private-links-event-type-${randomString()}`, + slug: `org-private-links-event-type-${randomString()}`, + length: 30, + locations: [], + team: { connect: { id: team.id } }, + }); + + await app.init(); + }); + + it("POST /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - create", async () => { + const body: CreatePrivateLinkInput = { maxUsageCount: 5 }; + const response = await request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) + .set("Authorization", "Bearer test") + .send(body) + .expect(201); + + expect(response.body.status).toBe(SUCCESS_STATUS); + expect(response.body.data.linkId).toBeDefined(); + expect(response.body.data.maxUsageCount).toBe(5); + expect(response.body.data.usageCount).toBeDefined(); + }); + + it("GET /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links - list", async () => { + const response = await request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) + .set("Authorization", "Bearer test") + .expect(200); + + expect(response.body.status).toBe(SUCCESS_STATUS); + expect(Array.isArray(response.body.data)).toBe(true); + }); + + it("PATCH /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - update", async () => { + // create first + const createResp = await request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) + .set("Authorization", "Bearer test") + .send({ maxUsageCount: 3 }) + .expect(201); + + const linkId = createResp.body.data.linkId as string; + + const response = await request(app.getHttpServer()) + .patch( + `/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}` + ) + .set("Authorization", "Bearer test") + .send({ maxUsageCount: 10 }) + .expect(200); + + expect(response.body.status).toBe(SUCCESS_STATUS); + expect(response.body.data.maxUsageCount).toBe(10); + }); + + it("DELETE /v2/organizations/:orgId/teams/:teamId/event-types/:eventTypeId/private-links/:linkId - delete", async () => { + // create first + const createResp = await request(app.getHttpServer()) + .post(`/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links`) + .set("Authorization", "Bearer test") + .send({ maxUsageCount: 2 }) + .expect(201); + + const linkId = createResp.body.data.linkId as string; + + const response = await request(app.getHttpServer()) + .delete( + `/v2/organizations/${org.id}/teams/${team.id}/event-types/${eventType.id}/private-links/${linkId}` + ) + .set("Authorization", "Bearer test") + .expect(200); + + expect(response.body.status).toBe(SUCCESS_STATUS); + expect(response.body.data.linkId).toBe(linkId); + }); + + afterAll(async () => { + try { + if (eventType?.id) { + await eventTypesFixture.delete(eventType.id); + } + if (team?.id) { + await teamFixture.delete(team.id); + } + if (org?.id) { + await orgFixture.delete(org.id); + } + if (user?.email) { + await userFixture.deleteByEmail(user.email); + } + } catch {} + await app.close(); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/organizations.module.ts b/apps/api/v2/src/modules/organizations/organizations.module.ts index 32d9389675f5a6..9739679670ff2a 100644 --- a/apps/api/v2/src/modules/organizations/organizations.module.ts +++ b/apps/api/v2/src/modules/organizations/organizations.module.ts @@ -23,6 +23,8 @@ import { OrganizationsConferencingModule } from "@/modules/organizations/confere import { OrganizationsConferencingService } from "@/modules/organizations/conferencing/services/organizations-conferencing.service"; import { OrganizationsDelegationCredentialModule } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.module"; import { OrganizationsEventTypesController } from "@/modules/organizations/event-types/organizations-event-types.controller"; +import { EventTypesPrivateLinksModule } from "@/ee/event-types-private-links/event-types-private-links.module"; +import { OrganizationsEventTypesPrivateLinksController } from "@/ee/event-types-private-links/controllers/organizations-event-types-private-links.controller"; import { OrganizationsEventTypesRepository } from "@/modules/organizations/event-types/organizations-event-types.repository"; import { OutputTeamEventTypesResponsePipe } from "@/modules/organizations/event-types/pipes/team-event-types-response.transformer"; import { InputOrganizationsEventTypesService } from "@/modules/organizations/event-types/services/input.service"; @@ -91,6 +93,7 @@ import { Module } from "@nestjs/common"; OrganizationsStripeModule, OrganizationsTeamsRoutingFormsModule, OrganizationsConferencingModule, + EventTypesPrivateLinksModule, ], providers: [ OrganizationsRepository, @@ -177,6 +180,7 @@ import { Module } from "@nestjs/common"; OrganizationsTeamsSchedulesController, OrganizationsUsersOOOController, OrganizationTeamWorkflowsController, + OrganizationsEventTypesPrivateLinksController, ], }) export class OrganizationsModule {} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 0d4a9ac3990637..b6055d15dccc4e 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -4384,6 +4384,292 @@ ] } }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": { + "post": { + "operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink", + "summary": "Create a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePrivateLinkInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + }, + "get": { + "operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks", + "summary": "Get all private links for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPrivateLinksOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": { + "patch": { + "operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink", + "summary": "Update a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "linkId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink", + "summary": "Delete a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "linkId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/memberships": { "get": { "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", @@ -23787,7 +24073,214 @@ ] } } - } + } + }, + "CreatePrivateLinkInput": { + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "Expiration date for time-based links", + "format": "date-time", + "example": "2024-12-31T23:59:59.000Z" + }, + "maxUsageCount": { + "type": "number", + "description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).", + "example": 10, + "minimum": 1, + "default": 1 + } + } + }, + "TimeBasedPrivateLinkOutput": { + "type": "object", + "properties": { + "linkId": { + "type": "string", + "description": "The private link ID", + "example": "abc123def456" + }, + "eventTypeId": { + "type": "number", + "description": "Event type ID this link belongs to", + "example": 123 + }, + "isExpired": { + "type": "boolean", + "description": "Whether the link is currently expired", + "example": false + }, + "bookingUrl": { + "type": "string", + "description": "Full booking URL for this private link", + "format": "uri", + "example": "https://cal.com/d/abc123def456/30min" + }, + "expiresAt": { + "type": "string", + "description": "Expiration date for this time-based link", + "format": "date-time", + "example": "2025-12-31T23:59:59.000Z" + } + }, + "required": [ + "linkId", + "eventTypeId", + "isExpired", + "bookingUrl", + "expiresAt" + ] + }, + "UsageBasedPrivateLinkOutput": { + "type": "object", + "properties": { + "linkId": { + "type": "string", + "description": "The private link ID", + "example": "abc123def456" + }, + "eventTypeId": { + "type": "number", + "description": "Event type ID this link belongs to", + "example": 123 + }, + "isExpired": { + "type": "boolean", + "description": "Whether the link is currently expired", + "example": false + }, + "bookingUrl": { + "type": "string", + "description": "Full booking URL for this private link", + "format": "uri", + "example": "https://cal.com/d/abc123def456/30min" + }, + "maxUsageCount": { + "type": "number", + "description": "Maximum number of times this link can be used", + "example": 10 + }, + "usageCount": { + "type": "number", + "description": "Current usage count for this link", + "example": 3 + } + }, + "required": [ + "linkId", + "eventTypeId", + "isExpired", + "bookingUrl", + "maxUsageCount", + "usageCount" + ] + }, + "CreatePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "description": "Created private link data (either time-based or usage-based)", + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetPrivateLinksOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "type": "array", + "description": "Array of private links for the event type (mix of time-based and usage-based)", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdatePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "description": "Updated private link data (either time-based or usage-based)", + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "DeletePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "type": "object", + "description": "Deleted link information", + "properties": { + "linkId": { + "type": "string", + "example": "abc123def456" + }, + "message": { + "type": "string", + "example": "Private link deleted successfully" + } + } + } + }, + "required": [ + "status", + "data" + ] }, "StripConnectOutputDto": { "type": "object", @@ -25637,6 +26130,23 @@ "data" ] }, + "UpdatePrivateLinkBody": { + "type": "object", + "properties": { + "expiresAt": { + "format": "date-time", + "type": "string", + "description": "New expiration date for time-based links", + "example": "2024-12-31T23:59:59.000Z" + }, + "maxUsageCount": { + "type": "number", + "description": "New maximum number of times the link can be used", + "example": 10, + "minimum": 1 + } + } + }, "MeOrgOutput": { "type": "object", "properties": { @@ -28282,230 +28792,6 @@ "data" ] }, - "CreatePrivateLinkInput": { - "type": "object", - "properties": { - "expiresAt": { - "type": "string", - "description": "Expiration date for time-based links", - "format": "date-time", - "example": "2024-12-31T23:59:59.000Z" - }, - "maxUsageCount": { - "type": "number", - "description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).", - "example": 10, - "minimum": 1, - "default": 1 - } - } - }, - "TimeBasedPrivateLinkOutput": { - "type": "object", - "properties": { - "linkId": { - "type": "string", - "description": "The private link ID", - "example": "abc123def456" - }, - "eventTypeId": { - "type": "number", - "description": "Event type ID this link belongs to", - "example": 123 - }, - "isExpired": { - "type": "boolean", - "description": "Whether the link is currently expired", - "example": false - }, - "bookingUrl": { - "type": "string", - "description": "Full booking URL for this private link", - "format": "uri", - "example": "https://cal.com/d/abc123def456/30min" - }, - "expiresAt": { - "type": "string", - "description": "Expiration date for this time-based link", - "format": "date-time", - "example": "2025-12-31T23:59:59.000Z" - } - }, - "required": [ - "linkId", - "eventTypeId", - "isExpired", - "bookingUrl", - "expiresAt" - ] - }, - "UsageBasedPrivateLinkOutput": { - "type": "object", - "properties": { - "linkId": { - "type": "string", - "description": "The private link ID", - "example": "abc123def456" - }, - "eventTypeId": { - "type": "number", - "description": "Event type ID this link belongs to", - "example": 123 - }, - "isExpired": { - "type": "boolean", - "description": "Whether the link is currently expired", - "example": false - }, - "bookingUrl": { - "type": "string", - "description": "Full booking URL for this private link", - "format": "uri", - "example": "https://cal.com/d/abc123def456/30min" - }, - "maxUsageCount": { - "type": "number", - "description": "Maximum number of times this link can be used", - "example": 10 - }, - "usageCount": { - "type": "number", - "description": "Current usage count for this link", - "example": 3 - } - }, - "required": [ - "linkId", - "eventTypeId", - "isExpired", - "bookingUrl", - "maxUsageCount", - "usageCount" - ] - }, - "CreatePrivateLinkOutput": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Response status", - "example": "success" - }, - "data": { - "description": "Created private link data (either time-based or usage-based)", - "oneOf": [ - { - "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" - }, - { - "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" - } - ] - } - }, - "required": [ - "status", - "data" - ] - }, - "GetPrivateLinksOutput": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Response status", - "example": "success" - }, - "data": { - "type": "array", - "description": "Array of private links for the event type (mix of time-based and usage-based)", - "items": { - "oneOf": [ - { - "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" - }, - { - "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" - } - ] - } - } - }, - "required": [ - "status", - "data" - ] - }, - "UpdatePrivateLinkBody": { - "type": "object", - "properties": { - "expiresAt": { - "format": "date-time", - "type": "string", - "description": "New expiration date for time-based links", - "example": "2024-12-31T23:59:59.000Z" - }, - "maxUsageCount": { - "type": "number", - "description": "New maximum number of times the link can be used", - "example": 10, - "minimum": 1 - } - } - }, - "UpdatePrivateLinkOutput": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Response status", - "example": "success" - }, - "data": { - "description": "Updated private link data (either time-based or usage-based)", - "oneOf": [ - { - "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" - }, - { - "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" - } - ] - } - }, - "required": [ - "status", - "data" - ] - }, - "DeletePrivateLinkOutput": { - "type": "object", - "properties": { - "status": { - "type": "string", - "description": "Response status", - "example": "success" - }, - "data": { - "type": "object", - "description": "Deleted link information", - "properties": { - "linkId": { - "type": "string", - "example": "abc123def456" - }, - "message": { - "type": "string", - "example": "Private link deleted successfully" - } - } - } - }, - "required": [ - "status", - "data" - ] - }, "UserWebhookOutputDto": { "type": "object", "properties": { diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index 718464697cc4f1..00840056634e26 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -4193,6 +4193,292 @@ "tags": ["Orgs / Teams / Event Types"] } }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links": { + "post": { + "operationId": "OrganizationsEventTypesPrivateLinksController_createPrivateLink", + "summary": "Create a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePrivateLinkInput" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreatePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + }, + "get": { + "operationId": "OrganizationsEventTypesPrivateLinksController_getPrivateLinks", + "summary": "Get all private links for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPrivateLinksOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/event-types/{eventTypeId}/private-links/{linkId}": { + "patch": { + "operationId": "OrganizationsEventTypesPrivateLinksController_updatePrivateLink", + "summary": "Update a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "linkId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdatePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + }, + "delete": { + "operationId": "OrganizationsEventTypesPrivateLinksController_deletePrivateLink", + "summary": "Delete a private link for a team event type", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "For non-platform customers - value must be `Bearer ` where `` is api key prefixed with cal_", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-secret-key", + "in": "header", + "description": "For platform customers - OAuth client secret key", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "x-cal-client-id", + "in": "header", + "description": "For platform customers - OAuth client ID", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "eventTypeId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "linkId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletePrivateLinkOutput" + } + } + } + } + }, + "tags": [ + "Orgs / Teams / Event Types / Private Links" + ] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/memberships": { "get": { "operationId": "OrganizationsTeamsMembershipsController_getAllOrgTeamMemberships", @@ -21786,6 +22072,213 @@ } } }, + "CreatePrivateLinkInput": { + "type": "object", + "properties": { + "expiresAt": { + "type": "string", + "description": "Expiration date for time-based links", + "format": "date-time", + "example": "2024-12-31T23:59:59.000Z" + }, + "maxUsageCount": { + "type": "number", + "description": "Maximum number of times the link can be used. If omitted and expiresAt is not provided, defaults to 1 (one time use).", + "example": 10, + "minimum": 1, + "default": 1 + } + } + }, + "TimeBasedPrivateLinkOutput": { + "type": "object", + "properties": { + "linkId": { + "type": "string", + "description": "The private link ID", + "example": "abc123def456" + }, + "eventTypeId": { + "type": "number", + "description": "Event type ID this link belongs to", + "example": 123 + }, + "isExpired": { + "type": "boolean", + "description": "Whether the link is currently expired", + "example": false + }, + "bookingUrl": { + "type": "string", + "description": "Full booking URL for this private link", + "format": "uri", + "example": "https://cal.com/d/abc123def456/30min" + }, + "expiresAt": { + "type": "string", + "description": "Expiration date for this time-based link", + "format": "date-time", + "example": "2025-12-31T23:59:59.000Z" + } + }, + "required": [ + "linkId", + "eventTypeId", + "isExpired", + "bookingUrl", + "expiresAt" + ] + }, + "UsageBasedPrivateLinkOutput": { + "type": "object", + "properties": { + "linkId": { + "type": "string", + "description": "The private link ID", + "example": "abc123def456" + }, + "eventTypeId": { + "type": "number", + "description": "Event type ID this link belongs to", + "example": 123 + }, + "isExpired": { + "type": "boolean", + "description": "Whether the link is currently expired", + "example": false + }, + "bookingUrl": { + "type": "string", + "description": "Full booking URL for this private link", + "format": "uri", + "example": "https://cal.com/d/abc123def456/30min" + }, + "maxUsageCount": { + "type": "number", + "description": "Maximum number of times this link can be used", + "example": 10 + }, + "usageCount": { + "type": "number", + "description": "Current usage count for this link", + "example": 3 + } + }, + "required": [ + "linkId", + "eventTypeId", + "isExpired", + "bookingUrl", + "maxUsageCount", + "usageCount" + ] + }, + "CreatePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "description": "Created private link data (either time-based or usage-based)", + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "GetPrivateLinksOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "type": "array", + "description": "Array of private links for the event type (mix of time-based and usage-based)", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + } + }, + "required": [ + "status", + "data" + ] + }, + "UpdatePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "description": "Updated private link data (either time-based or usage-based)", + "oneOf": [ + { + "$ref": "#/components/schemas/TimeBasedPrivateLinkOutput" + }, + { + "$ref": "#/components/schemas/UsageBasedPrivateLinkOutput" + } + ] + } + }, + "required": [ + "status", + "data" + ] + }, + "DeletePrivateLinkOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "Response status", + "example": "success" + }, + "data": { + "type": "object", + "description": "Deleted link information", + "properties": { + "linkId": { + "type": "string", + "example": "abc123def456" + }, + "message": { + "type": "string", + "example": "Private link deleted successfully" + } + } + } + }, + "required": [ + "status", + "data" + ] + }, "StripConnectOutputDto": { "type": "object", "properties": { @@ -23368,6 +23861,23 @@ }, "required": ["status", "data"] }, + "UpdatePrivateLinkBody": { + "type": "object", + "properties": { + "expiresAt": { + "format": "date-time", + "type": "string", + "description": "New expiration date for time-based links", + "example": "2024-12-31T23:59:59.000Z" + }, + "maxUsageCount": { + "type": "number", + "description": "New maximum number of times the link can be used", + "example": 10, + "minimum": 1 + } + } + }, "MeOrgOutput": { "type": "object", "properties": { diff --git a/yarn.lock b/yarn.lock index 56a53682be2c99..fae32f702bba94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2500,7 +2500,7 @@ __metadata: "@axiomhq/winston": ^1.2.0 "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.305" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.306" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -3559,13 +3559,13 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.305": - version: 0.0.305 - resolution: "@calcom/platform-libraries@npm:0.0.305" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.306": + version: 0.0.306 + resolution: "@calcom/platform-libraries@npm:0.0.306" dependencies: "@calcom/features": "*" "@calcom/lib": "*" - checksum: 047b9a45c384e22ef62b40aa3deafa3b952304eefacf48d74bd39b1b92bfed13609bbd46a909a1ddae8a7cb8ad341aaf0912281a25d701c926998090a2a13fe8 + checksum: 602b8b446508429096b8e03e9edbe1ff744cf7c7af3fbe8a683aaaf0d3c96d1cd30ff356f72682449ca072beadaae95fdaa5c44b26e30cfce3bb838f519216f9 languageName: node linkType: hard