feat: verified resources endpoint [v2] (#21006)
* feat: verified resources endpoint [v2] * add tests and fixes * chore: add endpoint throttler decorator and handling * chore: code review comments * chore: code review comments * chore: code review comments * bump platform libraries * chore: add tests and fix issues
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.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": "*",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { RateLimitType } from "@/lib/throttler-guard";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
export const Throttle = Reflector.createDecorator<RateLimitType>();
|
||||
@@ -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<typeof rateLimitSchema>;
|
||||
export type RateLimitType = z.infer<typeof rateLimitSchema>;
|
||||
const rateLimitsSchema = z.array(rateLimitSchema);
|
||||
|
||||
const sixtySecondsMs = 60 * 1000;
|
||||
@@ -52,7 +53,7 @@ export class CustomThrottlerGuard extends ThrottlerGuard {
|
||||
|
||||
protected async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
|
||||
const { context } = requestProps;
|
||||
|
||||
const throttleOptions = this.reflector.get(Throttle, context.getHandler());
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const IP = request?.headers?.["cf-connecting-ip"] ?? request?.headers?.["CF-Connecting-IP"] ?? request.ip;
|
||||
const response = context.switchToHttp().getResponse<Response>();
|
||||
@@ -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<boolean> {
|
||||
const rateLimits = await this.getRateLimitsForApiKeyTracker(tracker);
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
})
|
||||
|
||||
+2
@@ -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)
|
||||
|
||||
+236
@@ -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: "<html><body>Mocked Email Content</body></html>",
|
||||
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();
|
||||
});
|
||||
});
|
||||
+256
@@ -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<RequestEmailVerificationOutput> {
|
||||
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<RequestPhoneVerificationOutput> {
|
||||
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<TeamVerifiedEmailOutput> {
|
||||
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<TeamVerifiedPhoneOutput> {
|
||||
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<TeamVerifiedEmailsOutput> {
|
||||
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<TeamVerifiedPhonesOutput> {
|
||||
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<TeamVerifiedEmailOutput> {
|
||||
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<TeamVerifiedPhoneOutput> {
|
||||
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||
teamId,
|
||||
id
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||
};
|
||||
}
|
||||
}
|
||||
+229
@@ -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<RequestEmailVerificationOutput> {
|
||||
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<RequestPhoneVerificationOutput> {
|
||||
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<TeamVerifiedEmailOutput> {
|
||||
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<TeamVerifiedPhoneOutput> {
|
||||
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<TeamVerifiedEmailsOutput> {
|
||||
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<TeamVerifiedPhonesOutput> {
|
||||
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<TeamVerifiedEmailOutput> {
|
||||
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<TeamVerifiedPhoneOutput> {
|
||||
const verifiedPhoneNumber = await this.verifiedResourcesService.getTeamVerifiedPhoneNumberById(
|
||||
teamId,
|
||||
id
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||
};
|
||||
}
|
||||
}
|
||||
+196
@@ -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<RequestEmailVerificationOutput> {
|
||||
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<RequestPhoneVerificationOutput> {
|
||||
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<UserVerifiedEmailOutput> {
|
||||
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<UserVerifiedPhoneOutput> {
|
||||
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<UserVerifiedEmailsOutput> {
|
||||
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<UserVerifiedPhonesOutput> {
|
||||
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<UserVerifiedEmailOutput> {
|
||||
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<UserVerifiedPhoneOutput> {
|
||||
const verifiedPhoneNumber = await this.verifiedResourcesService.getUserVerifiedPhoneNumberById(
|
||||
userId,
|
||||
id
|
||||
);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(UserVerifiedPhoneOutputData, verifiedPhoneNumber),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
File diff suppressed because it is too large
Load Diff
+26
@@ -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 } });
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 };
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { TVerifyEmailCodeInputSchema } from "./verifyEmailCode.schema";
|
||||
|
||||
type VerifyEmailCodeOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id">;
|
||||
};
|
||||
input: TVerifyEmailCodeInputSchema;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user