diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 6a004e5e4f..c3b945ede5 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.178", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.179", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/lib/endpoint-throttler-decorator.ts b/apps/api/v2/src/lib/endpoint-throttler-decorator.ts new file mode 100644 index 0000000000..02e3b1b4a2 --- /dev/null +++ b/apps/api/v2/src/lib/endpoint-throttler-decorator.ts @@ -0,0 +1,4 @@ +import { RateLimitType } from "@/lib/throttler-guard"; +import { Reflector } from "@nestjs/core"; + +export const Throttle = Reflector.createDecorator(); diff --git a/apps/api/v2/src/lib/throttler-guard.ts b/apps/api/v2/src/lib/throttler-guard.ts index 66aad5cf3b..8f810f8490 100644 --- a/apps/api/v2/src/lib/throttler-guard.ts +++ b/apps/api/v2/src/lib/throttler-guard.ts @@ -1,5 +1,6 @@ import { getEnv } from "@/env"; import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis"; import { Inject, Injectable, Logger, UnauthorizedException } from "@nestjs/common"; @@ -22,7 +23,7 @@ const rateLimitSchema = z.object({ ttl: z.number(), blockDuration: z.number(), }); -type RateLimitType = z.infer; +export type RateLimitType = z.infer; const rateLimitsSchema = z.array(rateLimitSchema); const sixtySecondsMs = 60 * 1000; @@ -52,7 +53,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard { protected async handleRequest(requestProps: ThrottlerRequest): Promise { const { context } = requestProps; - + const throttleOptions = this.reflector.get(Throttle, context.getHandler()); const request = context.switchToHttp().getRequest(); const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip; const response = context.switchToHttp().getResponse(); @@ -63,6 +64,10 @@ export class CustomThrottlerGuard extends ThrottlerGuard { )}", OAuth client ID "${request.get(X_CAL_CLIENT_ID)}" and IP "${IP}"` ); + if (throttleOptions) { + return this.handleApiEndpointThrottle(tracker, throttleOptions, response); + } + if (tracker.startsWith("api_key_")) { return this.handleApiKeyRequest(tracker, response); } else { @@ -70,6 +75,15 @@ export class CustomThrottlerGuard extends ThrottlerGuard { } } + private async handleApiEndpointThrottle(tracker: string, options: RateLimitType, response: Response) { + const { isBlocked } = await this.incrementRateLimit(`${tracker}_${options.name}`, options, response); + if (isBlocked) { + throw new ThrottlerException("CustomThrottlerGuard - Too many requests. Please try again later."); + } + + return true; + } + private async handleApiKeyRequest(tracker: string, response: Response): Promise { const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker); diff --git a/apps/api/v2/src/modules/endpoints.module.ts b/apps/api/v2/src/modules/endpoints.module.ts index b7d0a6a67b..6febe08de0 100644 --- a/apps/api/v2/src/modules/endpoints.module.ts +++ b/apps/api/v2/src/modules/endpoints.module.ts @@ -11,6 +11,7 @@ import { OrganizationsUsersBookingsModule } from "@/modules/organizations/users/ import { RouterModule } from "@/modules/router/router.module"; import { StripeModule } from "@/modules/stripe/stripe.module"; import { TimezoneModule } from "@/modules/timezones/timezones.module"; +import { VerifiedResourcesModule } from "@/modules/verified-resources/verified-resources.module"; import type { MiddlewareConsumer, NestModule } from "@nestjs/common"; import { Module } from "@nestjs/common"; @@ -33,6 +34,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module"; OrganizationsUsersBookingsModule, OrganizationsBookingsModule, OrganizationsRoutingFormsModule, + VerifiedResourcesModule, RouterModule, ], }) diff --git a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts index efd3817a8e..22ed0bd9cd 100644 --- a/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts +++ b/apps/api/v2/src/modules/organizations/teams/routing-forms/controllers/organizations-teams-routing-forms-responses.controller.ts @@ -6,6 +6,7 @@ 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 { IsRoutingFormInTeam } from "@/modules/auth/guards/routing-forms/is-routing-form-in-team.guard"; import { IsTeamInOrg } from "@/modules/auth/guards/teams/is-team-in-org.guard"; import { GetRoutingFormResponsesParams } from "@/modules/organizations/routing-forms/inputs/get-routing-form-responses-params.input"; @@ -29,6 +30,7 @@ import { SUCCESS_STATUS } from "@calcom/platform-constants"; IsTeamInOrg, IsRoutingFormInTeam, PlatformPlanGuard, + RolesGuard, IsAdminAPIEnabledGuard ) @ApiHeader(API_KEY_HEADER) diff --git a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts new file mode 100644 index 0000000000..81729cb5e3 --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.e2e-spec.ts @@ -0,0 +1,236 @@ +import { bootstrap } from "@/app"; +import { AppModule } from "@/app.module"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { TokensModule } from "@/modules/tokens/tokens.module"; +import { UsersModule } from "@/modules/users/users.module"; +import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; +import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; +import { + TeamVerifiedEmailOutput, + TeamVerifiedEmailsOutput, +} from "@/modules/verified-resources/outputs/verified-email.output"; +import { + TeamVerifiedPhoneOutput, + TeamVerifiedPhonesOutput, +} from "@/modules/verified-resources/outputs/verified-phone.output"; +import { INestApplication } from "@nestjs/common"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { Test } from "@nestjs/testing"; +import { TOTP as TOTPtoMock } from "@otplib/core"; +import { User } from "@prisma/client"; +import { totp } from "otplib"; +import * as request from "supertest"; +import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture"; +import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture"; +import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture"; +import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture"; +import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture"; +import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; +import { VerifiedResourcesRepositoryFixtures } from "test/fixtures/repository/verified-resources.repository.fixture"; +import { randomString } from "test/utils/randomString"; + +import { AttendeeVerifyEmail } from "@calcom/platform-libraries/emails"; +import { Team } from "@calcom/prisma/client"; + +jest.spyOn(totp, "generate").mockImplementation(function () { + return "1234"; +}); + +jest.spyOn(TOTPtoMock.prototype, "check").mockImplementation(function () { + return true; +}); + +jest + .spyOn(AttendeeVerifyEmail.prototype as any, "getNodeMailerPayload") + .mockImplementation(async function () { + return { + to: `testnotrealemail@notreal.com`, + from: `testnotrealemail@notreal.com`, + subject: "No Subject", + html: "Mocked Email Content", + text: "body", + }; + }); + +describe("Organizations Teams Verified Resources", () => { + let app: INestApplication; + + let userRepositoryFixture: UserRepositoryFixture; + let organizationsRepositoryFixture: OrganizationRepositoryFixture; + let verifiedResourcesRepositoryFixtures: VerifiedResourcesRepositoryFixtures; + let teamsRepositoryFixture: TeamRepositoryFixture; + let profileRepositoryFixture: ProfileRepositoryFixture; + let apiKeysRepositoryFixture: ApiKeysRepositoryFixture; + let membershipsRepositoryFixture: MembershipRepositoryFixture; + + let org: Team; + let orgTeam: Team; + const emailToVerify = `org-team-e2e-verified-resources-${randomString()}@example.com`; + const phoneToVerify = "+37255556666"; + const authEmail = `organizations-verified-resources-responses-user-${randomString()}@api.com`; + let user: User; + let apiKeyString: string; + let verifiedEmailId: number; + let verifiedPhoneId: number; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule, PrismaModule, UsersModule, TokensModule], + }).compile(); + + verifiedResourcesRepositoryFixtures = new VerifiedResourcesRepositoryFixtures(moduleRef); + userRepositoryFixture = new UserRepositoryFixture(moduleRef); + organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef); + teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef); + profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef); + apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef); + membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef); + + org = await organizationsRepositoryFixture.create({ + name: `organizations-verified-resources-responses-organization-${randomString()}`, + isOrganization: true, + }); + + user = await userRepositoryFixture.create({ + email: authEmail, + username: authEmail, + }); + + const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null); + apiKeyString = keyString; + + orgTeam = await teamsRepositoryFixture.create({ + name: `organizations-verified-resources-responses-team-${randomString()}`, + isOrganization: false, + parent: { connect: { id: org.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: org.id } }, + }); + + await membershipsRepositoryFixture.create({ + role: "ADMIN", + user: { connect: { id: user.id } }, + team: { connect: { id: orgTeam.id } }, + }); + + await profileRepositoryFixture.create({ + uid: `usr-${user.id}`, + username: authEmail, + organization: { + connect: { + id: org.id, + }, + }, + user: { + connect: { + id: user.id, + }, + }, + }); + + verifiedPhoneId = ( + await verifiedResourcesRepositoryFixtures.createPhone({ + phoneNumber: phoneToVerify, + user: { connect: { id: user.id } }, + team: { + connect: { + id: orgTeam.id, + }, + }, + }) + ).id; + + app = moduleRef.createNestApplication(); + bootstrap(app as NestExpressApplication); + + await app.init(); + }); + + it("should trigger email verification code", async () => { + return request(app.getHttpServer()) + .post( + `/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/request` + ) + .send({ email: emailToVerify } satisfies RequestEmailVerificationInput) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200); + }); + + it("should verify email", async () => { + return request(app.getHttpServer()) + .post( + `/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/verification-code/verify` + ) + .send({ email: emailToVerify, code: "1234" } satisfies VerifyEmailInput) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((res) => { + const response = res.body as TeamVerifiedEmailOutput; + verifiedEmailId = response.data.id; + expect(response.data.email).toEqual(emailToVerify); + expect(response.data.teamId).toEqual(orgTeam.id); + }); + }); + + it("should fetch verified email by id", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails/${verifiedEmailId}`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((res) => { + const response = res.body as TeamVerifiedEmailOutput; + expect(response.data.email).toEqual(emailToVerify); + expect(response.data.teamId).toEqual(orgTeam.id); + }); + }); + + it("should fetch verified number by id", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones/${verifiedPhoneId}`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((res) => { + const response = res.body as TeamVerifiedPhoneOutput; + expect(response.data.phoneNumber).toEqual(phoneToVerify); + expect(response.data.teamId).toEqual(orgTeam.id); + }); + }); + + it("should fetch verified emails", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/emails`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((res) => { + const response = res.body as TeamVerifiedEmailsOutput; + expect(response.data.find((verifiedEmail) => verifiedEmail.email === emailToVerify)).toBeDefined(); + expect(response.data.find((verifiedEmail) => verifiedEmail.teamId === orgTeam.id)).toBeDefined(); + }); + }); + + it("should fetch verified phones", async () => { + return request(app.getHttpServer()) + .get(`/v2/organizations/${org.id}/teams/${orgTeam.id}/verified-resources/phones`) + .set({ Authorization: `Bearer cal_test_${apiKeyString}` }) + .expect(200) + .then((res) => { + const response = res.body as TeamVerifiedPhonesOutput; + expect( + response.data.find((verifiedPhone) => verifiedPhone.phoneNumber === phoneToVerify) + ).toBeDefined(); + expect(response.data.find((verifiedPhone) => verifiedPhone.teamId === orgTeam.id)).toBeDefined(); + }); + }); + + afterAll(async () => { + await verifiedResourcesRepositoryFixtures.deleteEmailById(verifiedEmailId); + await verifiedResourcesRepositoryFixtures.deletePhoneById(verifiedPhoneId); + await userRepositoryFixture.deleteByEmail(user.email); + await organizationsRepositoryFixture.delete(org.id); + await app.close(); + }); +}); diff --git a/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts new file mode 100644 index 0000000000..b3ee9f18fa --- /dev/null +++ b/apps/api/v2/src/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller.ts @@ -0,0 +1,256 @@ +import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; +import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.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 { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; +import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input"; +import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; +import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input"; +import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output"; +import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output"; +import { + TeamVerifiedEmailOutput, + TeamVerifiedEmailOutputData, + TeamVerifiedEmailsOutput, +} from "@/modules/verified-resources/outputs/verified-email.output"; +import { + TeamVerifiedPhoneOutput, + TeamVerifiedPhoneOutputData, + TeamVerifiedPhonesOutput, +} from "@/modules/verified-resources/outputs/verified-phone.output"; +import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Param, + ParseIntPipe, + Post, + Query, + UseGuards, +} from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { SkipTakePagination } from "@calcom/platform-types"; + +@Controller({ + path: "/v2/organizations/:orgId/teams/:teamId/verified-resources", +}) +@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard) +@ApiTags("Organization Team Verified Resources") +export class OrgTeamsVerifiedResourcesController { + constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {} + @ApiOperation({ + summary: "Request Email Verification Code", + description: `Sends a verification code to the email.`, + }) + @Roles("TEAM_ADMIN") + @Throttle({ + limit: 3, + ttl: 60000, + blockDuration: 60000, + name: "org_teams_verified_resources_emails_requests", + }) + @PlatformPlan("ESSENTIALS") + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/emails/verification-code/request") + @HttpCode(HttpStatus.OK) + async requestEmailVerificationCode( + @Body() body: RequestEmailVerificationInput, + @GetUser("username") username: string, + @GetUser("locale") locale: string + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode( + { username, locale }, + body.email + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Request Phone Number Verification Code", + description: `Sends a verification code to the phone number.`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Throttle({ + limit: 3, + ttl: 60000, + blockDuration: 60000, + name: "org_teams_verified_resources_phones_requests", + }) + @Post("/phones/verification-code/request") + @HttpCode(HttpStatus.OK) + async requestPhoneVerificationCode( + @Body() body: RequestPhoneVerificationInput + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode( + body.phone + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Verify an email for an org team.", + description: `Use code to verify an email`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @Post("/emails/verification-code/verify") + @HttpCode(HttpStatus.OK) + async verifyEmail( + @Body() body: VerifyEmailInput, + @GetUser("id") userId: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.verifyEmail( + userId, + body.email, + body.code, + teamId + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Verify a phone number for an org team.", + description: `Use code to verify a phone number`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @Post("/phones/verification-code/verify") + @Throttle({ + limit: 3, + ttl: 60000, + blockDuration: 60000, + name: "org_teams_verified_resources_phones_verify", + }) + @HttpCode(HttpStatus.OK) + async verifyPhoneNumber( + @Body() body: VerifyPhoneInput, + @GetUser("id") userId: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedPhone = await this.verifiedResourcesService.verifyPhone( + userId, + body.phone, + body.code, + teamId + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone), + }; + } + + @ApiOperation({ + summary: "Get list of verified emails of an org team.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @Get("/emails") + @HttpCode(HttpStatus.OK) + async getVerifiedEmails( + @Param("teamId", ParseIntPipe) teamId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails( + teamId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)), + }; + } + + @ApiOperation({ + summary: "Get list of verified phone numbers of an org team.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @PlatformPlan("ESSENTIALS") + @Get("/phones") + @Roles("TEAM_ADMIN") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneNumbers( + @Param("teamId", ParseIntPipe) teamId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers( + teamId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedPhoneNumbers.map((verifiedPhoneNumber) => + plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber) + ), + }; + } + + @ApiOperation({ + summary: "Get verified email of an org team by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @Get("/emails/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedEmailById( + @Param("id") id: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Get verified phone number of an org team by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @PlatformPlan("ESSENTIALS") + @Get("/phones/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneById( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("id") id: number + ): Promise { + const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById( + teamId, + id + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber), + }; + } +} diff --git a/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts b/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts new file mode 100644 index 0000000000..a3b16c2e9f --- /dev/null +++ b/apps/api/v2/src/modules/teams/verified-resources/teams-verified-resources.controller.ts @@ -0,0 +1,229 @@ +import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard"; +import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; +import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input"; +import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; +import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input"; +import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output"; +import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output"; +import { + TeamVerifiedEmailOutput, + TeamVerifiedEmailOutputData, + TeamVerifiedEmailsOutput, +} from "@/modules/verified-resources/outputs/verified-email.output"; +import { + TeamVerifiedPhoneOutput, + TeamVerifiedPhoneOutputData, + TeamVerifiedPhonesOutput, +} from "@/modules/verified-resources/outputs/verified-phone.output"; +import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Param, + ParseIntPipe, + Post, + Query, + UseGuards, +} from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { SkipTakePagination } from "@calcom/platform-types"; + +@Controller({ + path: "/v2/teams/:teamId/verified-resources", +}) +@UseGuards(ApiAuthGuard, RolesGuard) +@ApiTags("Teams Verified Resources") +export class TeamsVerifiedResourcesController { + constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {} + @ApiOperation({ + summary: "Request Email Verification Code", + description: `Sends a verification code to the Email.`, + }) + @Roles("TEAM_ADMIN") + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/emails/verification-code/request") + @Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_emails_requests" }) + @HttpCode(HttpStatus.OK) + async requestEmailVerificationCode( + @Body() body: RequestEmailVerificationInput, + @GetUser("username") username: string, + @GetUser("locale") locale: string + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode( + { username, locale }, + body.email + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Request Phone Number Verification Code", + description: `Sends a verification code to the phone number.`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Post("/phones/verification-code/request") + @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_requests" }) + @HttpCode(HttpStatus.OK) + async requestPhoneVerificationCode( + @Body() body: RequestPhoneVerificationInput + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode( + body.phone + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Verify an email for a team.", + description: `Use code to verify an email`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Post("/emails/verification-code/verify") + @HttpCode(HttpStatus.OK) + async verifyEmail( + @Body() body: VerifyEmailInput, + @GetUser("id") userId: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.verifyEmail( + userId, + body.email, + body.code, + teamId + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Verify a phone number for an org team.", + description: `Use code to verify a phone number`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Post("/phones/verification-code/verify") + @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "teams_verified_resources_phones_verify" }) + @HttpCode(HttpStatus.OK) + async verifyPhoneNumber( + @Body() body: VerifyPhoneInput, + @GetUser("id") userId: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedPhone = await this.verifiedResourcesService.verifyPhone( + userId, + body.phone, + body.code, + teamId + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhone), + }; + } + + @ApiOperation({ + summary: "Get list of verified emails of a team.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Get("/emails") + @HttpCode(HttpStatus.OK) + async getVerifiedEmails( + @Param("teamId", ParseIntPipe) teamId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedEmails = await this.verifiedResourcesService.getTeamVerifiedEmails( + teamId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedEmails.map((verifiedEmail) => plainToClass(TeamVerifiedEmailOutputData, verifiedEmail)), + }; + } + + @ApiOperation({ + summary: "Get list of verified phone numbers of a team.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Get("/phones") + @Roles("TEAM_ADMIN") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneNumbers( + @Param("teamId", ParseIntPipe) teamId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedPhoneNumbers = await this.verifiedResourcesService.getTeamVerifiedPhoneNumbers( + teamId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedPhoneNumbers.map((verifiedPhoneNumber) => + plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber) + ), + }; + } + + @ApiOperation({ + summary: "Get verified email of a team by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Get("/emails/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedEmailById( + @Param("id") id: number, + @Param("teamId", ParseIntPipe) teamId: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.getTeamVerifiedEmailById(teamId, id); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Get verified phone number of a team by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Roles("TEAM_ADMIN") + @Get("/phones/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneById( + @Param("teamId", ParseIntPipe) teamId: number, + @Param("id") id: number + ): Promise { + const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById( + teamId, + id + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber), + }; + } +} diff --git a/apps/api/v2/src/modules/verified-resources/controllers/users-verified-resources.controller.ts b/apps/api/v2/src/modules/verified-resources/controllers/users-verified-resources.controller.ts new file mode 100644 index 0000000000..25dac497b4 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/controllers/users-verified-resources.controller.ts @@ -0,0 +1,196 @@ +import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers"; +import { Throttle } from "@/lib/endpoint-throttler-decorator"; +import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; +import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard"; +import { RequestEmailVerificationInput } from "@/modules/verified-resources/inputs/request-email-verification.input"; +import { RequestPhoneVerificationInput } from "@/modules/verified-resources/inputs/request-phone-verification.input"; +import { VerifyEmailInput } from "@/modules/verified-resources/inputs/verify-email.input"; +import { VerifyPhoneInput } from "@/modules/verified-resources/inputs/verify-phone.input"; +import { RequestEmailVerificationOutput } from "@/modules/verified-resources/outputs/request-email-verification-output"; +import { RequestPhoneVerificationOutput } from "@/modules/verified-resources/outputs/request-phone-verification-output"; +import { + UserVerifiedEmailOutput, + UserVerifiedEmailOutputData, + UserVerifiedEmailsOutput, +} from "@/modules/verified-resources/outputs/verified-email.output"; +import { + UserVerifiedPhoneOutput, + UserVerifiedPhoneOutputData, + UserVerifiedPhonesOutput, +} from "@/modules/verified-resources/outputs/verified-phone.output"; +import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiHeader, ApiOperation, ApiTags } from "@nestjs/swagger"; +import { plainToClass } from "class-transformer"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; +import { SkipTakePagination } from "@calcom/platform-types"; + +@Controller({ + path: "/v2/verified-resources", +}) +@UseGuards(ApiAuthGuard) +@ApiTags("Verified Resources") +export class UserVerifiedResourcesController { + constructor(private readonly verifiedResourcesService: VerifiedResourcesService) {} + @ApiOperation({ + summary: "Request Email Verification Code", + description: `Sends a verification code to the email.`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/emails/verification-code/request") + @HttpCode(HttpStatus.OK) + @Throttle({ limit: 5, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_emails_requests" }) + async requestEmailVerificationCode( + @Body() body: RequestEmailVerificationInput, + @GetUser("username") username: string, + @GetUser("locale") locale: string + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestEmailVerificationCode( + { username, locale }, + body.email + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Request Phone Number Verification Code", + description: `Sends a verification code to the phone number.`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/phones/verification-code/request") + @HttpCode(HttpStatus.OK) + @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_requests" }) + async requestPhoneVerificationCode( + @Body() body: RequestPhoneVerificationInput + ): Promise { + const verificationCodeRequest = await this.verifiedResourcesService.requestPhoneVerificationCode( + body.phone + ); + + return { + status: verificationCodeRequest ? SUCCESS_STATUS : ERROR_STATUS, + }; + } + + @ApiOperation({ + summary: "Verify an email.", + description: `Use code to verify an email`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/emails/verification-code/verify") + @Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "users_verified_resources_phones_verify" }) + @HttpCode(HttpStatus.OK) + async verifyEmail( + @Body() body: VerifyEmailInput, + @GetUser("id") userId: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.verifyEmail(userId, body.email, body.code); + return { + status: SUCCESS_STATUS, + data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Verify a phone number.", + description: `Use code to verify a phone number`, + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Post("/phones/verification-code/verify") + @HttpCode(HttpStatus.OK) + async verifyPhoneNumber( + @Body() body: VerifyPhoneInput, + @GetUser("id") userId: number + ): Promise { + const verifiedPhone = await this.verifiedResourcesService.verifyPhone(userId, body.phone, body.code); + return { + status: SUCCESS_STATUS, + data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhone), + }; + } + + @ApiOperation({ + summary: "Get list of verified emails.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Get("/emails") + @HttpCode(HttpStatus.OK) + async getVerifiedEmails( + @GetUser("id") userId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedEmails = await this.verifiedResourcesService.getUserVerifiedEmails( + userId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedEmails.map((verifiedEmail) => plainToClass(UserVerifiedEmailOutputData, verifiedEmail)), + }; + } + + @ApiOperation({ + summary: "Get list of verified phone numbers.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Get("/phones") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneNumbers( + @GetUser("id") userId: number, + @Query() pagination: SkipTakePagination + ): Promise { + const verifiedPhoneNumbers = await this.verifiedResourcesService.getUserVerifiedPhoneNumbers( + userId, + pagination?.skip ?? 0, + pagination?.take ?? 250 + ); + return { + status: SUCCESS_STATUS, + data: verifiedPhoneNumbers.map((verifiedPhoneNumber) => + plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber) + ), + }; + } + + @ApiOperation({ + summary: "Get verified email by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Get("/emails/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedEmailById( + @GetUser("id") userId: number, + @Param("id") id: number + ): Promise { + const verifiedEmail = await this.verifiedResourcesService.getUserVerifiedEmailById(userId, id); + return { + status: SUCCESS_STATUS, + data: plainToClass(UserVerifiedEmailOutputData, verifiedEmail), + }; + } + + @ApiOperation({ + summary: "Get verified phone number by id.", + }) + @ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER) + @Get("/phones/:id") + @HttpCode(HttpStatus.OK) + async getVerifiedPhoneById( + @GetUser("id") userId: number, + @Param("id") id: number + ): Promise { + const verifiedPhoneNumber = await this.verifiedResourcesService.getUserVerifiedPhoneNumberById( + userId, + id + ); + return { + status: SUCCESS_STATUS, + data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber), + }; + } +} diff --git a/apps/api/v2/src/modules/verified-resources/inputs/request-email-verification.input.ts b/apps/api/v2/src/modules/verified-resources/inputs/request-email-verification.input.ts new file mode 100644 index 0000000000..70726861c6 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/inputs/request-email-verification.input.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose } from "class-transformer"; +import { IsEmail } from "class-validator"; + +export class RequestEmailVerificationInput { + @ApiProperty({ + type: String, + description: "Email to verify.", + example: "acme@example.com", + }) + @IsEmail() + @Expose() + email!: string; +} diff --git a/apps/api/v2/src/modules/verified-resources/inputs/request-phone-verification.input.ts b/apps/api/v2/src/modules/verified-resources/inputs/request-phone-verification.input.ts new file mode 100644 index 0000000000..1ae2169b44 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/inputs/request-phone-verification.input.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose } from "class-transformer"; +import { IsPhoneNumber } from "class-validator"; + +export class RequestPhoneVerificationInput { + @ApiProperty({ + type: String, + description: "Phone number to verify.", + example: "+372 5555 6666", + }) + @IsPhoneNumber() + @Expose() + phone!: string; +} diff --git a/apps/api/v2/src/modules/verified-resources/inputs/verify-email.input.ts b/apps/api/v2/src/modules/verified-resources/inputs/verify-email.input.ts new file mode 100644 index 0000000000..81aa0df59d --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/inputs/verify-email.input.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose } from "class-transformer"; +import { IsEmail, IsString } from "class-validator"; + +export class VerifyEmailInput { + @ApiProperty({ + type: String, + description: "Email to verify.", + example: "example@acme.com", + }) + @IsEmail() + @Expose() + email!: string; + + @ApiProperty({ + type: String, + description: "verification code sent to the email to verify", + example: "1ABG2C", + }) + @Expose() + @IsString() + code!: string; +} diff --git a/apps/api/v2/src/modules/verified-resources/inputs/verify-phone.input.ts b/apps/api/v2/src/modules/verified-resources/inputs/verify-phone.input.ts new file mode 100644 index 0000000000..2b00ea23e6 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/inputs/verify-phone.input.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose } from "class-transformer"; +import { IsEmail, IsPhoneNumber, IsString } from "class-validator"; + +export class VerifyPhoneInput { + @ApiProperty({ + type: String, + description: "phone number to verify.", + example: "+37255556666", + }) + @IsPhoneNumber() + @Expose() + phone!: string; + + @ApiProperty({ + type: String, + description: "verification code sent to the phone number to verify", + example: "1ABG2C", + }) + @Expose() + @IsString() + code!: string; +} diff --git a/apps/api/v2/src/modules/verified-resources/outputs/request-email-verification-output.ts b/apps/api/v2/src/modules/verified-resources/outputs/request-email-verification-output.ts new file mode 100644 index 0000000000..7635f6db4a --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/outputs/request-email-verification-output.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum } from "class-validator"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; + +export class RequestEmailVerificationOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; +} diff --git a/apps/api/v2/src/modules/verified-resources/outputs/request-phone-verification-output.ts b/apps/api/v2/src/modules/verified-resources/outputs/request-phone-verification-output.ts new file mode 100644 index 0000000000..1fbfc61928 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/outputs/request-phone-verification-output.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum } from "class-validator"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; + +export class RequestPhoneVerificationOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; +} diff --git a/apps/api/v2/src/modules/verified-resources/outputs/verified-email.output.ts b/apps/api/v2/src/modules/verified-resources/outputs/verified-email.output.ts new file mode 100644 index 0000000000..a79cd5403b --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/outputs/verified-email.output.ts @@ -0,0 +1,135 @@ +import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ApiProperty } from "@nestjs/swagger"; +import { Expose, Type } from "class-transformer"; +import { + IsArray, + IsEnum, + IsNotEmptyObject, + IsNumber, + IsOptional, + IsString, + ValidateNested, +} from "class-validator"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; + +export class UserVerifiedEmailOutputData { + @Expose() + @IsNumber() + @ApiProperty({ + description: "The unique identifier for the verified email.", + example: 789, + }) + id!: number; + + @Expose() + @IsString() + @ApiProperty({ + description: "The verified email address.", + example: "user@example.com", + format: "email", + }) + email!: string; + + @Expose() + @IsNumber() + @ApiProperty({ + description: "The ID of the associated user, if applicable.", + example: 45, + }) + userId!: number; +} + +export class TeamVerifiedEmailOutputData { + @Expose() + @IsNumber() + @ApiProperty({ + description: "The unique identifier for the verified email.", + example: 789, + }) + id!: number; + + @Expose() + @IsString() + @ApiProperty({ + description: "The verified email address.", + example: "user@example.com", + format: "email", + }) + email!: string; + + @Expose() + @IsNumber() + @ApiProperty({ + description: "The ID of the associated team, if applicable.", + example: 89, + }) + teamId!: number; + + @Expose() + @IsNumber() + @IsOptional() + @ApiProperty({ + description: "The ID of the associated user, if applicable.", + example: 45, + nullable: true, + required: false, + }) + userId?: number | null; +} + +export class TeamVerifiedEmailOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @Type(() => TeamVerifiedEmailOutputData) + data!: TeamVerifiedEmailOutputData; +} + +export class UserVerifiedEmailOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @Type(() => UserVerifiedEmailOutputData) + data!: UserVerifiedEmailOutputData; +} + +export class TeamVerifiedEmailsOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TeamVerifiedEmailOutputData) + data!: TeamVerifiedEmailOutputData[]; +} + +export class UserVerifiedEmailsOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => UserVerifiedEmailOutputData) + data!: UserVerifiedEmailOutputData[]; +} diff --git a/apps/api/v2/src/modules/verified-resources/outputs/verified-phone.output.ts b/apps/api/v2/src/modules/verified-resources/outputs/verified-phone.output.ts new file mode 100644 index 0000000000..a1d1fd8b4a --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/outputs/verified-phone.output.ts @@ -0,0 +1,135 @@ +import { ScheduleOutput } from "@/ee/schedules/schedules_2024_04_15/outputs/schedule.output"; +import { ApiProperty } from "@nestjs/swagger"; +import { Expose, Type } from "class-transformer"; +import { + IsArray, + IsEnum, + IsNotEmptyObject, + IsNumber, + IsString, + ValidateNested, + IsOptional, +} from "class-validator"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; + +export class UserVerifiedPhoneOutputData { + @Expose() + @IsNumber() + @ApiProperty({ + description: "The unique identifier for the verified email.", + example: 789, + }) + id!: number; + + @Expose() + @IsString() + @ApiProperty({ + description: "The verified phone number.", + example: "+37255556666", + format: "phone", + }) + phoneNumber!: string; + + @Expose() + @IsNumber() + @ApiProperty({ + description: "The ID of the associated user, if applicable.", + example: 45, + }) + userId!: number; +} + +export class TeamVerifiedPhoneOutputData { + @Expose() + @IsNumber() + @ApiProperty({ + description: "The unique identifier for the verified email.", + example: 789, + }) + id!: number; + + @Expose() + @IsString() + @ApiProperty({ + description: "The verified phone number.", + example: "+37255556666", + format: "phone", + }) + phoneNumber!: string; + + @Expose() + @IsNumber() + @IsOptional() + @ApiProperty({ + description: "The ID of the associated user, if applicable.", + example: 45, + nullable: true, + required: false, + }) + userId?: number | null; + + @Expose() + @IsNumber() + @ApiProperty({ + description: "The ID of the associated team, if applicable.", + example: 89, + }) + teamId!: number; +} + +export class UserVerifiedPhoneOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @Type(() => UserVerifiedPhoneOutputData) + data!: UserVerifiedPhoneOutputData; +} + +export class TeamVerifiedPhoneOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @Type(() => TeamVerifiedPhoneOutputData) + data!: TeamVerifiedPhoneOutputData; +} + +export class UserVerifiedPhonesOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => UserVerifiedPhoneOutputData) + data!: UserVerifiedPhoneOutputData[]; +} + +export class TeamVerifiedPhonesOutput { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ScheduleOutput, + }) + @IsNotEmptyObject() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => TeamVerifiedPhoneOutputData) + data!: TeamVerifiedPhoneOutputData[]; +} diff --git a/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts b/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts new file mode 100644 index 0000000000..b0ba2b434a --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/services/verified-resources.service.ts @@ -0,0 +1,120 @@ +import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; +import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository"; +import { BadRequestException, ConflictException, Injectable } from "@nestjs/common"; + +import { + verifyPhoneNumber, + sendVerificationCode as sendPhoneVerificationCode, +} from "@calcom/platform-libraries"; +import { sendEmailVerificationByCode, verifyEmailCodeHandler } from "@calcom/platform-libraries/emails"; + +@Injectable() +export class VerifiedResourcesService { + constructor( + private readonly usersVerifiedResourcesRepository: UsersVerifiedResourcesRepository, + private readonly teamsVerifiedResourcesRepository: TeamsVerifiedResourcesRepository + ) {} + + async requestEmailVerificationCode(user: { username: string; locale: string }, email: string) { + const res = await sendEmailVerificationByCode({ + email: email, + language: user.locale, + username: user.username, + isVerifyingEmail: true, + }); + + if (res.ok && !res.skipped) { + return true; + } + + if (!res.ok) { + throw new BadRequestException("Email could not be verified."); + } + + if (res.skipped) { + throw new ConflictException("Email is already verified."); + } + + return true; + } + + async requestPhoneVerificationCode(phone: string) { + try { + await sendPhoneVerificationCode(phone); + } catch (err) { + throw new BadRequestException("Could not send verification code to this phone number."); + } + + return true; + } + + async verifyPhone(userId: number, phone: string, code: string, teamId?: number) { + const result = await verifyPhoneNumber(phone, code, userId, teamId); + + if (result) { + return teamId + ? await this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phone, teamId) + : await this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumber(userId, phone); + } + + throw new BadRequestException("Could not verify phone number."); + } + + async verifyEmail(userId: number, email: string, code: string, teamId?: number) { + const isValidToken = await verifyEmailCodeHandler({ + ctx: { user: { id: userId } }, + input: { email, code, teamId }, + }); + if (isValidToken) { + const verifiedEmail = teamId + ? await this.teamsVerifiedResourcesRepository.getTeamVerifiedEmail(userId, email, teamId) + : await this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email); + return verifiedEmail; + } + throw new BadRequestException("Invalid Verification Code"); + } + + async getUserVerifiedEmailById(userId: number, id: number) { + return this.usersVerifiedResourcesRepository.getUserVerifiedEmailById(userId, id); + } + + async getTeamVerifiedEmailById(teamId: number, id: number) { + return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmailById(id, teamId); + } + + async getVerifiedEmail(userId: number, email: string) { + return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, email); + } + + async getUserVerifiedEmails(userId: number, skip = 0, take = 250) { + return this.usersVerifiedResourcesRepository.getUserVerifiedEmails(userId, skip, take); + } + + async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) { + return this.teamsVerifiedResourcesRepository.getTeamVerifiedEmails(teamId, skip, take); + } + + async getUserVerifiedPhoneNumberById(userId: number, id: number) { + return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumberById(userId, id); + } + + async getTeamVerifiedPhoneNumberById(teamId: number, id: number) { + return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumberById(id, teamId); + } + + async getUserVerifiedPhoneNumber(userId: number, phoneNumber: string) { + return this.usersVerifiedResourcesRepository.getUserVerifiedEmail(userId, phoneNumber); + } + + async getTeamVerifiedPhoneNumber(userId: number, phoneNumber: string, teamId: number) { + return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumber(userId, phoneNumber, teamId); + } + + async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) { + return this.usersVerifiedResourcesRepository.getUserVerifiedPhoneNumbers(userId, skip, take); + } + + async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) { + return this.teamsVerifiedResourcesRepository.getTeamVerifiedPhoneNumbers(teamId, skip, take); + } +} diff --git a/apps/api/v2/src/modules/verified-resources/teams-verified-resources.repository.ts b/apps/api/v2/src/modules/verified-resources/teams-verified-resources.repository.ts new file mode 100644 index 0000000000..b638dbcefe --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/teams-verified-resources.repository.ts @@ -0,0 +1,66 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class TeamsVerifiedResourcesRepository { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getTeamVerifiedEmails(teamId: number, skip = 0, take = 250) { + return this.dbRead.prisma.verifiedEmail.findMany({ + where: { + teamId, + }, + skip, + take, + }); + } + + async getTeamVerifiedPhoneNumbers(teamId: number, skip = 0, take = 250) { + return this.dbRead.prisma.verifiedNumber.findMany({ + where: { + teamId, + }, + skip, + take, + }); + } + + async getTeamVerifiedEmail(userId: number, email: string, teamId: number) { + return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({ + where: { + userId, + email, + teamId, + }, + }); + } + + async getTeamVerifiedPhoneNumber(userId: number, phone: string, teamId: number) { + return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({ + where: { + userId, + phoneNumber: phone, + teamId, + }, + }); + } + + async getTeamVerifiedEmailById(id: number, teamId: number) { + return this.dbRead.prisma.verifiedEmail.findFirst({ + where: { + id, + teamId, + }, + }); + } + + async getTeamVerifiedPhoneNumberById(id: number, teamId: number) { + return this.dbRead.prisma.verifiedNumber.findFirst({ + where: { + id, + teamId, + }, + }); + } +} diff --git a/apps/api/v2/src/modules/verified-resources/users-verified-resources.repository.ts b/apps/api/v2/src/modules/verified-resources/users-verified-resources.repository.ts new file mode 100644 index 0000000000..713f9680d8 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/users-verified-resources.repository.ts @@ -0,0 +1,64 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class UsersVerifiedResourcesRepository { + constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {} + + async getUserVerifiedEmails(userId: number, skip = 0, take = 250) { + return this.dbRead.prisma.verifiedEmail.findMany({ + where: { + userId, + }, + skip, + take, + }); + } + + async getUserVerifiedPhoneNumber(userId: number, phone: string) { + return this.dbRead.prisma.verifiedNumber.findFirstOrThrow({ + where: { + userId, + phoneNumber: phone, + }, + }); + } + + async getUserVerifiedPhoneNumbers(userId: number, skip = 0, take = 250) { + return this.dbRead.prisma.verifiedNumber.findMany({ + where: { + userId, + }, + skip, + take, + }); + } + + async getUserVerifiedEmail(userId: number, email: string) { + return this.dbRead.prisma.verifiedEmail.findFirstOrThrow({ + where: { + userId, + email, + }, + }); + } + + async getUserVerifiedEmailById(userId: number, id: number) { + return this.dbRead.prisma.verifiedEmail.findFirst({ + where: { + userId, + id, + }, + }); + } + + async getUserVerifiedPhoneNumberById(userId: number, id: number) { + return this.dbRead.prisma.verifiedNumber.findFirst({ + where: { + userId, + id, + }, + }); + } +} diff --git a/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts b/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts new file mode 100644 index 0000000000..b40c2d32d6 --- /dev/null +++ b/apps/api/v2/src/modules/verified-resources/verified-resources.module.ts @@ -0,0 +1,39 @@ +import { AppsRepository } from "@/modules/apps/apps.repository"; +import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; +import { MembershipsRepository } from "@/modules/memberships/memberships.repository"; +import { OrganizationsRepository } from "@/modules/organizations/index/organizations.repository"; +import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/index/organizations-teams.repository"; +import { OrgTeamsVerifiedResourcesController } from "@/modules/organizations/teams/verified-resources/org-teams-verified-resources.controller"; +import { PrismaModule } from "@/modules/prisma/prisma.module"; +import { RedisModule } from "@/modules/redis/redis.module"; +import { StripeService } from "@/modules/stripe/stripe.service"; +import { TeamsVerifiedResourcesController } from "@/modules/teams/verified-resources/teams-verified-resources.controller"; +import { UsersRepository } from "@/modules/users/users.repository"; +import { UserVerifiedResourcesController } from "@/modules/verified-resources/controllers/users-verified-resources.controller"; +import { VerifiedResourcesService } from "@/modules/verified-resources/services/verified-resources.service"; +import { TeamsVerifiedResourcesRepository } from "@/modules/verified-resources/teams-verified-resources.repository"; +import { UsersVerifiedResourcesRepository } from "@/modules/verified-resources/users-verified-resources.repository"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [PrismaModule, RedisModule], + controllers: [ + UserVerifiedResourcesController, + TeamsVerifiedResourcesController, + OrgTeamsVerifiedResourcesController, + ], + providers: [ + VerifiedResourcesService, + UsersVerifiedResourcesRepository, + TeamsVerifiedResourcesRepository, + MembershipsRepository, + OrganizationsTeamsRepository, + OrganizationsRepository, + StripeService, + AppsRepository, + CredentialsRepository, + UsersRepository, + ], + exports: [VerifiedResourcesService], +}) +export class VerifiedResourcesModule {} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index dd6a2dbc12..6ae330759a 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -9315,6 +9315,410 @@ ] } }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_verifyEmail", + "summary": "Verify an email for an org team.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number for an org team.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails of an org team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailsOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phone-numbers": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers of an org team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhonesOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/{id}": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email of an org team by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number of an org team by id.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Organization Team Verified Resources" + ] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": { "get": { "operationId": "OrganizationsStripeController_getTeamStripeConnectUrl", @@ -11203,6 +11607,766 @@ ] } }, + "/v2/teams/{teamId}/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the Email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "TeamsVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "TeamsVerifiedResourcesController_verifyEmail", + "summary": "Verify an email for a team.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "TeamsVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number for an org team.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/emails": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails of a team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailsOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/phone-numbers": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers of a team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhonesOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/emails/{id}": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email of a team by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/teams/{teamId}/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number of a team by id.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Teams Verified Resources" + ] + } + }, + "/v2/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "UserVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "UserVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "UserVerifiedResourcesController_verifyEmail", + "summary": "Verify an email.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "UserVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/emails": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails.", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailsOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/phone-numbers": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers.", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhonesOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/emails/{id}": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, + "/v2/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhoneOutput" + } + } + } + } + }, + "tags": [ + "Verified Resources" + ] + } + }, "/v2/webhooks": { "post": { "operationId": "WebhooksController_createWebhook", @@ -23463,6 +24627,395 @@ "status", "data" ] + }, + "RequestEmailVerificationInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "Email to verify.", + "example": "acme@example.com" + } + }, + "required": [ + "email" + ] + }, + "RequestEmailVerificationOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "RequestPhoneVerificationInput": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": "Phone number to verify.", + "example": "+372 5555 6666" + } + }, + "required": [ + "phone" + ] + }, + "RequestPhoneVerificationOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + } + }, + "required": [ + "status" + ] + }, + "VerifyEmailInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "Email to verify.", + "example": "example@acme.com" + }, + "code": { + "type": "string", + "description": "verification code sent to the email to verify", + "example": "1ABG2C" + } + }, + "required": [ + "email", + "code" + ] + }, + "WorkingHours": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "type": "number" + }, + "endTime": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "days", + "startTime", + "endTime" + ] + }, + "AvailabilityModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + } + }, + "required": [ + "id", + "days", + "startTime", + "endTime" + ] + }, + "ScheduleOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "isManaged": { + "type": "boolean" + }, + "workingHours": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkingHours" + } + }, + "schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel" + } + }, + "availability": { + "items": { + "type": "array" + }, + "type": "array" + }, + "timeZone": { + "type": "string" + }, + "dateOverrides": { + "type": "array", + "items": { + "type": "object" + } + }, + "isDefault": { + "type": "boolean" + }, + "isLastSchedule": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "isManaged", + "workingHours", + "schedule", + "availability", + "timeZone", + "isDefault", + "isLastSchedule", + "readOnly" + ] + }, + "UserVerifiedEmailOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "VerifyPhoneInput": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": "phone number to verify.", + "example": "+37255556666" + }, + "code": { + "type": "string", + "description": "verification code sent to the phone number to verify", + "example": "1ABG2C" + } + }, + "required": [ + "phone", + "code" + ] + }, + "UserVerifiedPhoneOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UserVerifiedEmailsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "UserVerifiedPhonesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamVerifiedEmailOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamVerifiedPhoneOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamVerifiedEmailsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] + }, + "TeamVerifiedPhonesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": [ + "status", + "data" + ] } } } diff --git a/apps/api/v2/test/fixtures/repository/verified-resources.repository.fixture.ts b/apps/api/v2/test/fixtures/repository/verified-resources.repository.fixture.ts new file mode 100644 index 0000000000..72c8b1722a --- /dev/null +++ b/apps/api/v2/test/fixtures/repository/verified-resources.repository.fixture.ts @@ -0,0 +1,26 @@ +import { PrismaReadService } from "@/modules/prisma/prisma-read.service"; +import { PrismaWriteService } from "@/modules/prisma/prisma-write.service"; +import { TestingModule } from "@nestjs/testing"; +import { Prisma } from "@prisma/client"; + +export class VerifiedResourcesRepositoryFixtures { + private prismaReadClient: PrismaReadService["prisma"]; + private prismaWriteClient: PrismaWriteService["prisma"]; + + constructor(private readonly module: TestingModule) { + this.prismaReadClient = module.get(PrismaReadService).prisma; + this.prismaWriteClient = module.get(PrismaWriteService).prisma; + } + + async createPhone(data: Prisma.VerifiedNumberCreateInput) { + return this.prismaWriteClient.verifiedNumber.create({ data }); + } + + async deleteEmailById(id: number) { + return this.prismaWriteClient.verifiedEmail.delete({ where: { id } }); + } + + async deletePhoneById(id: number) { + return this.prismaWriteClient.verifiedNumber.delete({ where: { id } }); + } +} diff --git a/docs/api-reference/v2/openapi.json b/docs/api-reference/v2/openapi.json index cf5ac9c778..de1f3d7bec 100644 --- a/docs/api-reference/v2/openapi.json +++ b/docs/api-reference/v2/openapi.json @@ -8872,6 +8872,394 @@ "tags": ["OAuth Clients"] } }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_verifyEmail", + "summary": "Verify an email for an org team.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "OrgTeamsVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number for an org team.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails of an org team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailsOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phone-numbers": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers of an org team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhonesOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/emails/{id}": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email of an org team by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, + "/v2/organizations/{orgId}/teams/{teamId}/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "OrgTeamsVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number of an org team by id.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Organization Team Verified Resources"] + } + }, "/v2/organizations/{orgId}/teams/{teamId}/stripe/connect": { "get": { "operationId": "OrganizationsStripeController_getTeamStripeConnectUrl", @@ -10685,6 +11073,734 @@ "tags": ["Teams / Memberships"] } }, + "/v2/teams/{teamId}/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "TeamsVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the Email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "TeamsVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "TeamsVerifiedResourcesController_verifyEmail", + "summary": "Verify an email for a team.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "TeamsVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number for an org team.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/emails": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails of a team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailsOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/phone-numbers": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers of a team.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhonesOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/emails/{id}": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email of a team by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/teams/{teamId}/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "TeamsVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number of a team by id.", + "parameters": [ + { + "name": "teamId", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TeamVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Teams Verified Resources"] + } + }, + "/v2/verified-resources/emails/verification-code/request": { + "post": { + "operationId": "UserVerifiedResourcesController_requestEmailVerificationCode", + "summary": "Request Email Verification Code", + "description": "Sends a verification code to the email.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestEmailVerificationOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/phones/verification-code/request": { + "post": { + "operationId": "UserVerifiedResourcesController_requestPhoneVerificationCode", + "summary": "Request Phone Number Verification Code", + "description": "Sends a verification code to the phone number.", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestPhoneVerificationOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/emails/verification-code/verify": { + "post": { + "operationId": "UserVerifiedResourcesController_verifyEmail", + "summary": "Verify an email.", + "description": "Use code to verify an email", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyEmailInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/phones/verification-code/verify": { + "post": { + "operationId": "UserVerifiedResourcesController_verifyPhoneNumber", + "summary": "Verify a phone number.", + "description": "Use code to verify a phone number", + "parameters": [ + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VerifyPhoneInput" + } + } + } + }, + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/emails": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedEmails", + "summary": "Get list of verified emails.", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailsOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/phone-numbers": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedPhoneNumbers", + "summary": "Get list of verified phone numbers.", + "parameters": [ + { + "name": "take", + "required": false, + "in": "query", + "description": "The number of items to return", + "example": 10, + "schema": { + "type": "number" + } + }, + { + "name": "skip", + "required": false, + "in": "query", + "description": "The number of items to skip", + "example": 0, + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhonesOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/emails/{id}": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedEmailById", + "summary": "Get verified email by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedEmailOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, + "/v2/verified-resources/phone-numbers/{id}": { + "get": { + "operationId": "UserVerifiedResourcesController_getVerifiedPhoneById", + "summary": "Get verified phone number by id.", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "number" + } + }, + { + "name": "Authorization", + "in": "header", + "description": "value must be `Bearer ` where `` is api key prefixed with cal_ or managed user access token", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserVerifiedPhoneOutput" + } + } + } + } + }, + "tags": ["Verified Resources"] + } + }, "/v2/webhooks": { "post": { "operationId": "WebhooksController_createWebhook", @@ -21374,6 +22490,318 @@ } }, "required": ["status", "data"] + }, + "RequestEmailVerificationInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "Email to verify.", + "example": "acme@example.com" + } + }, + "required": ["email"] + }, + "RequestEmailVerificationOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + } + }, + "required": ["status"] + }, + "RequestPhoneVerificationInput": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": "Phone number to verify.", + "example": "+372 5555 6666" + } + }, + "required": ["phone"] + }, + "RequestPhoneVerificationOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + } + }, + "required": ["status"] + }, + "VerifyEmailInput": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "Email to verify.", + "example": "example@acme.com" + }, + "code": { + "type": "string", + "description": "verification code sent to the email to verify", + "example": "1ABG2C" + } + }, + "required": ["email", "code"] + }, + "WorkingHours": { + "type": "object", + "properties": { + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "type": "number" + }, + "endTime": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + } + }, + "required": ["days", "startTime", "endTime"] + }, + "AvailabilityModel": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "userId": { + "type": "number", + "nullable": true + }, + "eventTypeId": { + "type": "number", + "nullable": true + }, + "days": { + "type": "array", + "items": { + "type": "number" + } + }, + "startTime": { + "format": "date-time", + "type": "string" + }, + "endTime": { + "format": "date-time", + "type": "string" + }, + "date": { + "format": "date-time", + "type": "string", + "nullable": true + }, + "scheduleId": { + "type": "number", + "nullable": true + } + }, + "required": ["id", "days", "startTime", "endTime"] + }, + "ScheduleOutput": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "isManaged": { + "type": "boolean" + }, + "workingHours": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkingHours" + } + }, + "schedule": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AvailabilityModel" + } + }, + "availability": { + "items": { + "type": "array" + }, + "type": "array" + }, + "timeZone": { + "type": "string" + }, + "dateOverrides": { + "type": "array", + "items": { + "type": "object" + } + }, + "isDefault": { + "type": "boolean" + }, + "isLastSchedule": { + "type": "boolean" + }, + "readOnly": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "isManaged", + "workingHours", + "schedule", + "availability", + "timeZone", + "isDefault", + "isLastSchedule", + "readOnly" + ] + }, + "UserVerifiedEmailOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "VerifyPhoneInput": { + "type": "object", + "properties": { + "phone": { + "type": "string", + "description": "phone number to verify.", + "example": "+37255556666" + }, + "code": { + "type": "string", + "description": "verification code sent to the phone number to verify", + "example": "1ABG2C" + } + }, + "required": ["phone", "code"] + }, + "UserVerifiedPhoneOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "UserVerifiedEmailsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "UserVerifiedPhonesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "TeamVerifiedEmailOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "TeamVerifiedPhoneOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "TeamVerifiedEmailsOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] + }, + "TeamVerifiedPhonesOutput": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": ["success", "error"] + }, + "data": { + "$ref": "#/components/schemas/ScheduleOutput" + } + }, + "required": ["status", "data"] } } } diff --git a/packages/platform/libraries/emails.ts b/packages/platform/libraries/emails.ts index d4f0860761..044ace5a0c 100644 --- a/packages/platform/libraries/emails.ts +++ b/packages/platform/libraries/emails.ts @@ -4,12 +4,17 @@ import AttendeeRequestEmail from "@calcom/emails/templates/attendee-request-emai import AttendeeRescheduledEmail from "@calcom/emails/templates/attendee-rescheduled-email"; import AttendeeScheduledEmail from "@calcom/emails/templates/attendee-scheduled-email"; import AttendeeUpdatedEmail from "@calcom/emails/templates/attendee-updated-email"; +import AttendeeVerifyEmail from "@calcom/emails/templates/attendee-verify-email"; import OrganizerCancelledEmail from "@calcom/emails/templates/organizer-cancelled-email"; import OrganizerReassignedEmail from "@calcom/emails/templates/organizer-reassigned-email"; import OrganizerRequestEmail from "@calcom/emails/templates/organizer-request-email"; import OrganizerRescheduledEmail from "@calcom/emails/templates/organizer-rescheduled-email"; import OrganizerScheduledEmail from "@calcom/emails/templates/organizer-scheduled-email"; +import { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail"; import { sendSignupToOrganizationEmail } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils"; +import { verifyEmailCodeHandler } from "@calcom/trpc/server/routers/viewer/workflows/verifyEmailCode.handler"; + +export { AttendeeVerifyEmail }; export { AttendeeScheduledEmail }; @@ -34,3 +39,7 @@ export { OrganizerRequestEmail }; export { AttendeeRequestEmail }; export { sendSignupToOrganizationEmail }; + +export { sendEmailVerificationByCode }; + +export { verifyEmailCodeHandler }; diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 0c410329ea..0cd3a34c2e 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -4,6 +4,10 @@ import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; import * as newBookingMethods from "@calcom/features/bookings/lib/handleNewBooking"; import { getClientSecretFromPayment } from "@calcom/features/ee/payments/pages/getClientSecretFromPayment"; +import { + verifyPhoneNumber, + sendVerificationCode, +} from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber"; import { handleCreatePhoneCall } from "@calcom/features/handleCreatePhoneCall"; import handleMarkNoShow from "@calcom/features/handleMarkNoShow"; import * as instantMeetingMethods from "@calcom/features/instant-meeting/handleInstantMeeting"; @@ -117,3 +121,5 @@ export { getCalendarLinks } from "@calcom/lib/bookings/getCalendarLinks"; export { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic"; export type { TFindTeamMembersMatchingAttributeLogicInputSchema } from "@calcom/trpc/server/routers/viewer/attributes/findTeamMembersMatchingAttributeLogic.schema"; export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; + +export { verifyPhoneNumber, sendVerificationCode }; diff --git a/packages/trpc/server/routers/viewer/workflows/verifyEmailCode.handler.ts b/packages/trpc/server/routers/viewer/workflows/verifyEmailCode.handler.ts index 8f2d6f2602..e41fad4e0e 100644 --- a/packages/trpc/server/routers/viewer/workflows/verifyEmailCode.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/verifyEmailCode.handler.ts @@ -10,7 +10,7 @@ import type { TVerifyEmailCodeInputSchema } from "./verifyEmailCode.schema"; type VerifyEmailCodeOptions = { ctx: { - user: NonNullable; + user: Pick, "id">; }; input: TVerifyEmailCodeInputSchema; };