feat: add send & verify code flow to booker atom when email verification is turned on (#23074)
* feat: extract tRPC verification logic to platform libraries and create v2 atoms endpoints - Extract verifyCodeUnAuthenticated, verifyCodeAuthenticated, and sendVerifyEmailCode logic from tRPC handlers into reusable platform library functions - Create VerificationAtomsService and AtomsVerificationController following atoms pattern - Add v2 endpoints: /atoms/verification/email/send-code, /atoms/verification/email/verify-code, /atoms/verification/email/verify-code-authenticated - Update useVerifyEmail and useVerifyCode hooks to use new v2 endpoints instead of verified-resources endpoints - Export verification functions from @calcom/platform-libraries following getPublicEvent pattern - Integrate platform-specific verification hooks into BookerPlatformWrapper Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * feat: update platform hooks to use v2 verification endpoints - Update useVerifyEmail hook to use correct response type for v2 endpoint - Update useVerifyCode hook to use v2 verification endpoint - All verification functions now properly exported from platform libraries - Type checking passes with no errors Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * fix: update v2 API to use workspace version of platform-libraries - Change package.json dependency from pinned version to workspace version - Add proper error handling to verification service with NestJS HTTP exceptions - Convert generic Error objects to BadRequestException and UnauthorizedException - Ensure verification endpoints return proper HTTP status codes instead of 500 errors Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * chore: update auto-generated files after package.json changes - Update OpenAPI documentation for v2 verification endpoints - Update yarn.lock after changing platform-libraries dependency Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * refactor: extract verification logic from tRPC handlers into exportable functions - Refactor sendVerifyEmailCode.handler.ts to extract core logic into sendVerifyEmailCode function - Refactor verifyCodeUnAuthenticated.handler.ts to extract core logic into verifyCodeUnAuthenticated function - Refactor organizations/verifyCode.handler.ts to extract core logic into verifyCodeAuthenticated function - Update platform/libraries/index.ts to import from refactored handler files instead of standalone verification.ts - Remove standalone verification.ts file as requested by user - Keep original tRPC handlers as wrappers that call the extracted functions - Maintain backward compatibility for existing tRPC endpoints Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * extract logic into seperate functions * Update index.ts * refactor: remove unused type imports from verification-atom service * feat: create v2 atoms input/output types for verification endpoints - Add SendVerificationEmailInput and VerifyEmailCodeInput with validation decorators - Add SendVerificationEmailOutput and VerifyEmailCodeOutput following v2 atoms patterns - Update atoms verification controller to use custom types instead of platform types - Auto-update openapi.json with new type definitions Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * fix: add missing type imports to verification service - Import ZVerifyCodeInputSchema from @calcom/prisma/zod-utils - Import VerifyCodeAuthenticatedInput from organizations handler - Import TSendVerifyEmailCodeSchema from sendVerifyEmailCode schema - Resolves CI build failure in API v2 tests Co-Authored-By: somay@cal.com <somaychauhan98@gmail.com> * feat: add email verification check endpoint and refactor verification flow * added Throttle * added chagelog * chore: update platform libraries to 0.0.317 * refactor: remove unused context and update email verification query key dependencies * chore: bump @calcom/platform-libraries from 0.0.318 to 0.0.319 * Update verifyCode.handler.ts * chore: bump @calcom/platform-libraries from 0.0.319 to 0.0.320 --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI
parent
f3d4eab951
commit
09cf8885cf
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
feat: add send & verify code flow to booker atom when email verification is turned on
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
"@calcom/prisma": "*",
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
|
||||
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomString } from "test/utils/randomString";
|
||||
@@ -27,7 +26,6 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
|
||||
import {
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
@@ -35,11 +33,9 @@ import {
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
|
||||
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
let apiKeyString: string;
|
||||
|
||||
let seatedEventTypeId: number;
|
||||
const maxRecurrenceCount = 3;
|
||||
|
||||
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
|
||||
|
||||
@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid: createdSeatedBooking.seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||
},
|
||||
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
guests: [],
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
|
||||
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
|
||||
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
|
||||
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
|
||||
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
|
||||
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
|
||||
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
|
||||
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
|
||||
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
|
||||
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
|
||||
AtomsRepository,
|
||||
UsersService,
|
||||
SchedulesAtomsService,
|
||||
VerificationAtomsService,
|
||||
RedisService,
|
||||
],
|
||||
exports: [EventTypesAtomService],
|
||||
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
|
||||
AtomsEventTypesController,
|
||||
AtomsConferencingAppsController,
|
||||
AtomsSchedulesController,
|
||||
AtomsVerificationController,
|
||||
],
|
||||
})
|
||||
export class AtomsModule {}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
UseGuards,
|
||||
Version,
|
||||
VERSION_NEUTRAL,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Get,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/atoms",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Atoms - verification endpoints for atoms")
|
||||
@DocsExcludeController(true)
|
||||
export class AtomsVerificationController {
|
||||
constructor(private readonly verificationService: VerificationAtomsService) {}
|
||||
|
||||
@Post("/verification/email/send-code")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
|
||||
async sendEmailVerificationCode(
|
||||
@Body() body: SendVerificationEmailInput
|
||||
): Promise<SendVerificationEmailOutput> {
|
||||
const result = await this.verificationService.sendEmailVerificationCode({
|
||||
email: body.email,
|
||||
username: body.username,
|
||||
language: body.language,
|
||||
isVerifyingEmail: body.isVerifyingEmail,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { sent: result.ok && !result.skipped },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/verification/email/check")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async checkEmailVerificationRequired(
|
||||
@Query() query: CheckEmailVerificationRequiredParams
|
||||
): Promise<ApiResponse<boolean>> {
|
||||
const required = await this.verificationService.checkEmailVerificationRequired({
|
||||
email: query.email,
|
||||
userSessionEmail: query.userSessionEmail,
|
||||
});
|
||||
|
||||
return {
|
||||
data: required,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/verification/email/verify-code")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
|
||||
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
|
||||
email: body.email,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { verified },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/verification/email/verify-code-authenticated")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async verifyEmailCodeAuthenticated(
|
||||
@Body() body: VerifyEmailCodeInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<VerifyEmailCodeOutput> {
|
||||
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
|
||||
email: body.email,
|
||||
code: body.code,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { verified },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEmail, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class CheckEmailVerificationRequiredParams {
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "user@example.com" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
userSessionEmail?: string;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
|
||||
|
||||
export class SendVerificationEmailInput {
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "johndoe" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
username?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: "en" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
language?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: true })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isVerifyingEmail?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEmail, IsString } from "class-validator";
|
||||
|
||||
export class VerifyEmailCodeInput {
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: "123456" })
|
||||
@IsString()
|
||||
code!: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class SendVerificationEmailData {
|
||||
@ApiProperty({ example: true })
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly sent!: boolean;
|
||||
}
|
||||
|
||||
export class SendVerificationEmailOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => SendVerificationEmailData)
|
||||
@Expose()
|
||||
@ApiProperty({ type: SendVerificationEmailData })
|
||||
readonly data!: SendVerificationEmailData;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsBoolean } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class VerifyEmailCodeData {
|
||||
@ApiProperty({ example: true })
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
readonly verified!: boolean;
|
||||
}
|
||||
|
||||
export class VerifyEmailCodeOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => VerifyEmailCodeData)
|
||||
@Expose()
|
||||
@ApiProperty({ type: VerifyEmailCodeData })
|
||||
readonly data!: VerifyEmailCodeData;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
verifyCodeUnAuthenticated,
|
||||
verifyCodeAuthenticated,
|
||||
sendEmailVerificationByCode,
|
||||
checkEmailVerificationRequired,
|
||||
} from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class VerificationAtomsService {
|
||||
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||
return await checkEmailVerificationRequired(input);
|
||||
}
|
||||
|
||||
async verifyEmailCodeUnAuthenticated(input: VerifyEmailCodeInput) {
|
||||
try {
|
||||
return await verifyCodeUnAuthenticated(input);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "invalid_code") {
|
||||
throw new BadRequestException("Invalid verification code");
|
||||
}
|
||||
if (error.message === "BAD_REQUEST") {
|
||||
throw new BadRequestException("Email and code are required");
|
||||
}
|
||||
}
|
||||
throw new BadRequestException("Verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
async verifyEmailCodeAuthenticated(user: UserWithProfile, input: VerifyEmailCodeInput) {
|
||||
try {
|
||||
return await verifyCodeAuthenticated({
|
||||
user,
|
||||
email: input.email,
|
||||
code: input.code,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "invalid_code") {
|
||||
throw new BadRequestException("Invalid verification code");
|
||||
}
|
||||
if (error.message === "BAD_REQUEST") {
|
||||
throw new BadRequestException("Email, code, and user ID are required");
|
||||
}
|
||||
}
|
||||
throw new UnauthorizedException("Verification failed");
|
||||
}
|
||||
}
|
||||
|
||||
async sendEmailVerificationCode(input: SendVerificationEmailInput) {
|
||||
return await sendEmailVerificationByCode({
|
||||
email: input.email,
|
||||
username: input.username,
|
||||
language: input.language,
|
||||
isVerifyingEmail: input.isVerifyingEmail,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,8 @@ import { useCalendarsBusyTimes } from "../hooks/useCalendarsBusyTimes";
|
||||
import { useConnectedCalendars } from "../hooks/useConnectedCalendars";
|
||||
import { useMe } from "../hooks/useMe";
|
||||
import { useSlots } from "../hooks/useSlots";
|
||||
import { useVerifyCode } from "../hooks/useVerifyCode";
|
||||
import { useVerifyEmail } from "../hooks/useVerifyEmail";
|
||||
import { AtomsWrapper } from "../src/components/atoms-wrapper";
|
||||
import type {
|
||||
BookerPlatformWrapperAtomPropsForIndividual,
|
||||
@@ -395,6 +397,23 @@ const BookerPlatformWrapperComponent = (
|
||||
handleSlotReservation,
|
||||
});
|
||||
|
||||
const verifyEmail = useVerifyEmail({
|
||||
email: bookerForm.formEmail,
|
||||
name: bookerForm.formName,
|
||||
requiresBookerEmailVerification: event?.data?.requiresBookerEmailVerification,
|
||||
onVerifyEmail: bookerForm.beforeVerifyEmail,
|
||||
});
|
||||
|
||||
const verifyCode = useVerifyCode({
|
||||
onSuccess: () => {
|
||||
if (!bookerForm.formEmail) return;
|
||||
|
||||
verifyEmail.setVerifiedEmail(bookerForm.formEmail);
|
||||
verifyEmail.setEmailVerificationModalVisible(false);
|
||||
handleBookEvent();
|
||||
},
|
||||
});
|
||||
|
||||
const { data: connectedCalendars, isPending: fetchingConnectedCalendars } = useConnectedCalendars({
|
||||
enabled: hasSession,
|
||||
});
|
||||
@@ -553,26 +572,13 @@ const BookerPlatformWrapperComponent = (
|
||||
return;
|
||||
},
|
||||
}}
|
||||
verifyEmail={{
|
||||
isEmailVerificationModalVisible: false,
|
||||
setEmailVerificationModalVisible: () => {
|
||||
return;
|
||||
},
|
||||
setVerifiedEmail: () => {
|
||||
return;
|
||||
},
|
||||
handleVerifyEmail: () => {
|
||||
return;
|
||||
},
|
||||
renderConfirmNotVerifyEmailButtonCond: true,
|
||||
isVerificationCodeSending: false,
|
||||
}}
|
||||
verifyEmail={verifyEmail}
|
||||
bookerForm={bookerForm}
|
||||
event={event}
|
||||
schedule={schedule}
|
||||
orgBannerUrl={bannerUrl ?? event.data?.bannerUrl}
|
||||
bookerLayout={{ ...bookerLayout, hideEventTypeDetails: hideEventMetadata }}
|
||||
verifyCode={undefined}
|
||||
verifyCode={verifyCode}
|
||||
isPlatform
|
||||
hasValidLicense={true}
|
||||
isBookingDryRun={isBookingDryRun ?? routingParams?.isBookingDryRun}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const QUERY_KEY = "get-me";
|
||||
* Access Token must be provided to CalProvider in order to use this hook
|
||||
* @returns The result of the query containing the user's profile.
|
||||
*/
|
||||
export const useMe = (isEmbed: boolean = false) => {
|
||||
export const useMe = (isEmbed = false) => {
|
||||
const pathname = `/${V2_ENDPOINTS.me}`;
|
||||
const me = useQuery({
|
||||
queryKey: [QUERY_KEY],
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiErrorResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../lib/http";
|
||||
|
||||
export type UseVerifyCodeReturnType = ReturnType<typeof useVerifyCode>;
|
||||
|
||||
type UseVerifyCodeProps = {
|
||||
onSuccess: (isVerified: boolean) => void;
|
||||
};
|
||||
|
||||
interface VerifyEmailInput {
|
||||
email: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
interface VerifyEmailResponse {
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export const useVerifyCode = ({ onSuccess }: UseVerifyCodeProps) => {
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [value, setValue] = useState("");
|
||||
const [hasVerified, setHasVerified] = useState(false);
|
||||
|
||||
const verifyCodeWithSessionRequiredMutation = useMutation<
|
||||
ApiSuccessResponse<VerifyEmailResponse>,
|
||||
ApiErrorResponse,
|
||||
VerifyEmailInput
|
||||
>({
|
||||
mutationFn: (props: VerifyEmailInput) => {
|
||||
return http
|
||||
.post<ApiResponse<{ verified: boolean }>>(
|
||||
"/atoms/verification/email/verify-code-authenticated",
|
||||
props
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.data.error?.message || "Invalid verification code");
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(data.data.verified);
|
||||
onSuccess(data.data.verified);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.error?.message?.includes("invalid") || err.error?.message?.includes("Invalid")) {
|
||||
setError("Code provided is invalid");
|
||||
} else {
|
||||
setError(err.error?.message || "Verification failed");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeWithSessionNotRequiredMutation = useMutation<
|
||||
ApiSuccessResponse<VerifyEmailResponse>,
|
||||
ApiErrorResponse,
|
||||
VerifyEmailInput
|
||||
>({
|
||||
mutationFn: (props: VerifyEmailInput) => {
|
||||
return http
|
||||
.post<ApiResponse<{ verified: boolean }>>("/atoms/verification/email/verify-code", props)
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.data.error?.message || "Invalid verification code");
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(data.data.verified);
|
||||
onSuccess(data.data.verified);
|
||||
},
|
||||
onError: (err) => {
|
||||
setIsPending(false);
|
||||
setHasVerified(false);
|
||||
if (err.error?.message?.includes("invalid") || err.error?.message?.includes("Invalid")) {
|
||||
setError("Code provided is invalid");
|
||||
} else {
|
||||
setError(err.error?.message || "Verification failed");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const verifyCodeWithSessionRequired = (code: string, email: string) => {
|
||||
verifyCodeWithSessionRequiredMutation.mutate({ code, email });
|
||||
};
|
||||
|
||||
const verifyCodeWithSessionNotRequired = (code: string, email: string) => {
|
||||
verifyCodeWithSessionNotRequiredMutation.mutate({ code, email });
|
||||
};
|
||||
|
||||
return {
|
||||
verifyCodeWithSessionRequired,
|
||||
verifyCodeWithSessionNotRequired,
|
||||
isPending,
|
||||
setIsPending,
|
||||
error,
|
||||
value,
|
||||
hasVerified,
|
||||
setValue,
|
||||
setHasVerified,
|
||||
resetErrors: () => setError(""),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiErrorResponse, ApiSuccessResponseWithoutData } from "@calcom/platform-types";
|
||||
|
||||
import { useMe } from "../hooks/useMe";
|
||||
import http from "../lib/http";
|
||||
|
||||
export interface IUseVerifyEmailProps {
|
||||
email: string;
|
||||
onVerifyEmail?: () => void;
|
||||
name?: string | { firstName: string; lastname?: string };
|
||||
requiresBookerEmailVerification?: boolean;
|
||||
}
|
||||
|
||||
export type UseVerifyEmailReturnType = ReturnType<typeof useVerifyEmail>;
|
||||
|
||||
interface RequestEmailVerificationInput {
|
||||
email: string;
|
||||
username?: string;
|
||||
}
|
||||
|
||||
export const useVerifyEmail = ({
|
||||
email,
|
||||
name,
|
||||
requiresBookerEmailVerification,
|
||||
onVerifyEmail,
|
||||
}: IUseVerifyEmailProps) => {
|
||||
const [isEmailVerificationModalVisible, setEmailVerificationModalVisible] = useState(false);
|
||||
const verifiedEmail = useBookerStore((state) => state.verifiedEmail);
|
||||
const setVerifiedEmail = useBookerStore((state) => state.setVerifiedEmail);
|
||||
const debouncedEmail = useDebounce(email, 600);
|
||||
const { data: user } = useMe();
|
||||
|
||||
const { data: isEmailVerificationRequired } = useQuery<boolean>({
|
||||
queryKey: ["isEmailVerificationRequired", debouncedEmail],
|
||||
queryFn: () => {
|
||||
return http
|
||||
?.get<ApiResponse<boolean>>("/atoms/verification/email/check", {
|
||||
params: {
|
||||
email: debouncedEmail,
|
||||
userSessionEmail: user?.data?.email || "",
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data.data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: !!debouncedEmail,
|
||||
});
|
||||
|
||||
const sendEmailVerificationMutation = useMutation<
|
||||
ApiSuccessResponseWithoutData,
|
||||
ApiErrorResponse,
|
||||
RequestEmailVerificationInput
|
||||
>({
|
||||
mutationFn: (props: RequestEmailVerificationInput) => {
|
||||
return http
|
||||
.post<ApiResponse<{ sent: boolean }>>("/atoms/verification/email/send-code", props)
|
||||
.then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data;
|
||||
}
|
||||
throw new Error(res.data.error?.message || "Failed to send verification email");
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setEmailVerificationModalVisible(true);
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error("Failed to send verification email:", err);
|
||||
},
|
||||
});
|
||||
|
||||
const handleVerifyEmail = () => {
|
||||
onVerifyEmail?.();
|
||||
sendEmailVerificationMutation.mutate({
|
||||
email,
|
||||
username: typeof name === "string" ? name : name?.firstName,
|
||||
});
|
||||
};
|
||||
|
||||
const isVerificationCodeSending = sendEmailVerificationMutation.isPending;
|
||||
|
||||
const renderConfirmNotVerifyEmailButtonCond =
|
||||
(!requiresBookerEmailVerification && !isEmailVerificationRequired) ||
|
||||
(email && verifiedEmail && verifiedEmail === email);
|
||||
|
||||
return {
|
||||
handleVerifyEmail,
|
||||
isEmailVerificationModalVisible,
|
||||
setEmailVerificationModalVisible,
|
||||
setVerifiedEmail,
|
||||
renderConfirmNotVerifyEmailButtonCond: Boolean(renderConfirmNotVerifyEmailButtonCond),
|
||||
isVerificationCodeSending,
|
||||
};
|
||||
};
|
||||
@@ -120,5 +120,13 @@ export { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner";
|
||||
|
||||
export { verifyPhoneNumber, sendVerificationCode };
|
||||
|
||||
export { verifyCodeUnAuthenticated } from "@calcom/trpc/server/routers/viewer/auth/verifyCodeUnAuthenticated.handler";
|
||||
|
||||
export { verifyCode as verifyCodeAuthenticated } from "@calcom/trpc/server/routers/viewer/organizations/verifyCode.handler";
|
||||
|
||||
export { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail";
|
||||
|
||||
export { checkEmailVerificationRequired } from "@calcom/trpc/server/routers/publicViewer/checkIfUserEmailVerificationRequired.handler";
|
||||
|
||||
export { TeamService } from "@calcom/lib/server/service/teamService";
|
||||
export { CacheService } from "@calcom/features/calendar-cache/lib/getShouldServeCache";
|
||||
|
||||
+10
-1
@@ -6,7 +6,16 @@ import type { TUserEmailVerificationRequiredSchema } from "./checkIfUserEmailVer
|
||||
const log = logger.getSubLogger({ prefix: ["checkIfUserEmailVerificationRequired"] });
|
||||
|
||||
export const userWithEmailHandler = async ({ input }: { input: TUserEmailVerificationRequiredSchema }) => {
|
||||
const { userSessionEmail, email } = input;
|
||||
return checkEmailVerificationRequired(input);
|
||||
};
|
||||
|
||||
export const checkEmailVerificationRequired = async ({
|
||||
userSessionEmail,
|
||||
email,
|
||||
}: {
|
||||
userSessionEmail?: string;
|
||||
email: string;
|
||||
}) => {
|
||||
const baseEmail = extractBaseEmail(email);
|
||||
|
||||
const blacklistedGuestEmails = process.env.BLACKLISTED_GUEST_EMAILS
|
||||
|
||||
@@ -3,10 +3,10 @@ import type { NextApiRequest } from "next";
|
||||
import { sendEmailVerificationByCode } from "@calcom/features/auth/lib/verifyEmail";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import getIP from "@calcom/lib/getIP";
|
||||
import { hashEmail, piiHasher } from "@calcom/lib/server/PiiHasher";
|
||||
|
||||
import type { TRPCContext } from "../../../createContext";
|
||||
import type { TSendVerifyEmailCodeSchema } from "./sendVerifyEmailCode.schema";
|
||||
import { hashEmail, piiHasher } from "@calcom/lib/server/PiiHasher";
|
||||
|
||||
type SendVerifyEmailCode = {
|
||||
input: TSendVerifyEmailCodeSchema;
|
||||
@@ -15,18 +15,25 @@ type SendVerifyEmailCode = {
|
||||
|
||||
export const sendVerifyEmailCodeHandler = async ({ input, req }: SendVerifyEmailCode) => {
|
||||
const identifier = req ? piiHasher.hash(getIP(req as NextApiRequest)) : hashEmail(input.email);
|
||||
return sendVerifyEmailCode({ input, identifier });
|
||||
};
|
||||
|
||||
export const sendVerifyEmailCode = async ({
|
||||
input,
|
||||
identifier,
|
||||
}: {
|
||||
input: TSendVerifyEmailCodeSchema;
|
||||
identifier: string;
|
||||
}) => {
|
||||
await checkRateLimitAndThrowError({
|
||||
rateLimitingType: "core",
|
||||
identifier: `emailVerifyByCode.${identifier}`,
|
||||
});
|
||||
|
||||
const email = await sendEmailVerificationByCode({
|
||||
return await sendEmailVerificationByCode({
|
||||
email: input.email,
|
||||
username: input.username,
|
||||
language: input.language,
|
||||
isVerifyingEmail: input.isVerifyingEmail,
|
||||
});
|
||||
|
||||
return email;
|
||||
};
|
||||
|
||||
@@ -10,6 +10,10 @@ type VerifyTokenOptions = {
|
||||
};
|
||||
|
||||
export const verifyCodeUnAuthenticatedHandler = async ({ input }: VerifyTokenOptions) => {
|
||||
return verifyCodeUnAuthenticated(input);
|
||||
};
|
||||
|
||||
export const verifyCodeUnAuthenticated = async (input: ZVerifyCodeInputSchema) => {
|
||||
const { email, code } = input;
|
||||
|
||||
if (!email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
@@ -3,12 +3,12 @@ import { createHash } from "crypto";
|
||||
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { hashEmail } from "@calcom/lib/server/PiiHasher";
|
||||
import { totpRawCheck } from "@calcom/lib/totp";
|
||||
import type { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { hashEmail } from "@calcom/lib/server/PiiHasher";
|
||||
|
||||
type VerifyCodeOptions = {
|
||||
ctx: {
|
||||
@@ -18,9 +18,22 @@ type VerifyCodeOptions = {
|
||||
};
|
||||
|
||||
export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
|
||||
const { email, code } = input;
|
||||
const { user } = ctx;
|
||||
return verifyCode({
|
||||
user: ctx.user,
|
||||
email: input.email,
|
||||
code: input.code,
|
||||
});
|
||||
};
|
||||
|
||||
export const verifyCode = async ({
|
||||
user,
|
||||
email,
|
||||
code,
|
||||
}: {
|
||||
user?: Pick<NonNullable<TrpcSessionUser>, "role">;
|
||||
email?: string;
|
||||
code?: string;
|
||||
}) => {
|
||||
if (!user || !email || !code) throw new TRPCError({ code: "BAD_REQUEST" });
|
||||
|
||||
if (!IS_PRODUCTION || process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
|
||||
Reference in New Issue
Block a user