Skip to content

Commit 09cf888

Browse files
feat: add send & verify code flow to booker atom when email verification is turned on (calcom#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>
1 parent f3d4eab commit 09cf888

22 files changed

Lines changed: 6326 additions & 77 deletions

.changeset/stale-tires-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calcom/atoms": minor
3+
---
4+
5+
feat: add send & verify code flow to booker atom when email verification is turned on

apps/api/v2/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"@axiomhq/winston": "^1.2.0",
3939
"@calcom/platform-constants": "*",
4040
"@calcom/platform-enums": "*",
41-
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.318",
41+
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.320",
4242
"@calcom/platform-types": "*",
4343
"@calcom/platform-utils": "*",
4444
"@calcom/prisma": "*",

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repo
1818
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
1919
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
2020
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
21-
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
2221
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
2322
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
2423
import { randomString } from "test/utils/randomString";
@@ -27,19 +26,16 @@ import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@cal
2726
import {
2827
CancelBookingInput_2024_08_13,
2928
CancelSeatedBookingInput_2024_08_13,
30-
CreateRecurringSeatedBookingOutput_2024_08_13,
3129
CreateSeatedBookingOutput_2024_08_13,
3230
GetBookingOutput_2024_08_13,
3331
GetBookingsOutput_2024_08_13,
3432
GetSeatedBookingOutput_2024_08_13,
3533
RescheduleSeatedBookingInput_2024_08_13,
3634
} from "@calcom/platform-types";
3735
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
38-
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
36+
import { Team } from "@calcom/prisma/client";
3937

4038
describe("Bookings Endpoints 2024-08-13", () => {
41-
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
42-
4339
describe("Seated bookings", () => {
4440
let app: INestApplication;
4541
let organization: Team;
@@ -48,7 +44,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
4844
let bookingsRepositoryFixture: BookingsRepositoryFixture;
4945
let schedulesService: SchedulesService_2024_04_15;
5046
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
51-
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
5247
let teamRepositoryFixture: TeamRepositoryFixture;
5348
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
5449
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
@@ -58,7 +53,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
5853
let apiKeyString: string;
5954

6055
let seatedEventTypeId: number;
61-
const maxRecurrenceCount = 3;
6256

6357
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
6458

@@ -83,7 +77,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
8377
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
8478
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
8579
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
86-
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
8780
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
8881
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
8982
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
@@ -190,6 +183,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
190183
absent: false,
191184
seatUid,
192185
bookingFieldsResponses: {
186+
guests: [],
193187
name: body.attendee.name,
194188
...body.bookingFieldsResponses,
195189
},
@@ -264,6 +258,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
264258
absent: false,
265259
seatUid: createdSeatedBooking.seatUid,
266260
bookingFieldsResponses: {
261+
guests: [],
267262
name: createdSeatedBooking.attendees[0].name,
268263
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
269264
},
@@ -278,6 +273,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
278273
absent: false,
279274
seatUid,
280275
bookingFieldsResponses: {
276+
guests: [],
281277
name: body.attendee.name,
282278
...body.bookingFieldsResponses,
283279
},
@@ -517,6 +513,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
517513
absent: false,
518514
seatUid,
519515
bookingFieldsResponses: {
516+
guests: [],
520517
name: body.attendee.name,
521518
...body.bookingFieldsResponses,
522519
},

apps/api/v2/src/ee/event-types-private-links/event-types-private-links.module.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
22
import { EventTypeOwnershipGuard } from "@/modules/event-types/guards/event-type-ownership.guard";
3-
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
43
import { PrismaModule } from "@/modules/prisma/prisma.module";
54
import { TokensModule } from "@/modules/tokens/tokens.module";
65
import { Module } from "@nestjs/common";

apps/api/v2/src/modules/atoms/atoms.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import { AtomsConferencingAppsController } from "@/modules/atoms/controllers/ato
44
import { AtomsController } from "@/modules/atoms/controllers/atoms.controller";
55
import { AtomsEventTypesController } from "@/modules/atoms/controllers/atoms.event-types.controller";
66
import { AtomsSchedulesController } from "@/modules/atoms/controllers/atoms.schedules.controller";
7+
import { AtomsVerificationController } from "@/modules/atoms/controllers/atoms.verification.controller";
78
import { AttributesAtomsService } from "@/modules/atoms/services/attributes-atom.service";
89
import { ConferencingAtomsService } from "@/modules/atoms/services/conferencing-atom.service";
910
import { EventTypesAtomService } from "@/modules/atoms/services/event-types-atom.service";
1011
import { SchedulesAtomsService } from "@/modules/atoms/services/schedules-atom.service";
12+
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
1113
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
1214
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
1315
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
@@ -32,6 +34,7 @@ import { Module } from "@nestjs/common";
3234
AtomsRepository,
3335
UsersService,
3436
SchedulesAtomsService,
37+
VerificationAtomsService,
3538
RedisService,
3639
],
3740
exports: [EventTypesAtomService],
@@ -40,6 +43,7 @@ import { Module } from "@nestjs/common";
4043
AtomsEventTypesController,
4144
AtomsConferencingAppsController,
4245
AtomsSchedulesController,
46+
AtomsVerificationController,
4347
],
4448
})
4549
export class AtomsModule {}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
2+
import { Throttle } from "@/lib/endpoint-throttler-decorator";
3+
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
4+
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
5+
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
6+
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
7+
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
8+
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
9+
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
10+
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
11+
import { UserWithProfile } from "@/modules/users/users.repository";
12+
import {
13+
Controller,
14+
Post,
15+
Body,
16+
UseGuards,
17+
Version,
18+
VERSION_NEUTRAL,
19+
HttpCode,
20+
HttpStatus,
21+
Get,
22+
Query,
23+
} from "@nestjs/common";
24+
import { ApiTags as DocsTags, ApiExcludeController as DocsExcludeController } from "@nestjs/swagger";
25+
26+
import { SUCCESS_STATUS } from "@calcom/platform-constants";
27+
import { ApiResponse } from "@calcom/platform-types";
28+
29+
@Controller({
30+
path: "/v2/atoms",
31+
version: API_VERSIONS_VALUES,
32+
})
33+
@DocsTags("Atoms - verification endpoints for atoms")
34+
@DocsExcludeController(true)
35+
export class AtomsVerificationController {
36+
constructor(private readonly verificationService: VerificationAtomsService) {}
37+
38+
@Post("/verification/email/send-code")
39+
@Version(VERSION_NEUTRAL)
40+
@HttpCode(HttpStatus.OK)
41+
@Throttle({ limit: 3, ttl: 60000, blockDuration: 60000, name: "atoms_verification_email_send_code" })
42+
async sendEmailVerificationCode(
43+
@Body() body: SendVerificationEmailInput
44+
): Promise<SendVerificationEmailOutput> {
45+
const result = await this.verificationService.sendEmailVerificationCode({
46+
email: body.email,
47+
username: body.username,
48+
language: body.language,
49+
isVerifyingEmail: body.isVerifyingEmail,
50+
});
51+
52+
return {
53+
data: { sent: result.ok && !result.skipped },
54+
status: SUCCESS_STATUS,
55+
};
56+
}
57+
58+
@Get("/verification/email/check")
59+
@Version(VERSION_NEUTRAL)
60+
@HttpCode(HttpStatus.OK)
61+
async checkEmailVerificationRequired(
62+
@Query() query: CheckEmailVerificationRequiredParams
63+
): Promise<ApiResponse<boolean>> {
64+
const required = await this.verificationService.checkEmailVerificationRequired({
65+
email: query.email,
66+
userSessionEmail: query.userSessionEmail,
67+
});
68+
69+
return {
70+
data: required,
71+
status: SUCCESS_STATUS,
72+
};
73+
}
74+
75+
@Post("/verification/email/verify-code")
76+
@Version(VERSION_NEUTRAL)
77+
@HttpCode(HttpStatus.OK)
78+
async verifyEmailCode(@Body() body: VerifyEmailCodeInput): Promise<VerifyEmailCodeOutput> {
79+
const verified = await this.verificationService.verifyEmailCodeUnAuthenticated({
80+
email: body.email,
81+
code: body.code,
82+
});
83+
84+
return {
85+
data: { verified },
86+
status: SUCCESS_STATUS,
87+
};
88+
}
89+
90+
@Post("/verification/email/verify-code-authenticated")
91+
@Version(VERSION_NEUTRAL)
92+
@UseGuards(ApiAuthGuard)
93+
@HttpCode(HttpStatus.OK)
94+
async verifyEmailCodeAuthenticated(
95+
@Body() body: VerifyEmailCodeInput,
96+
@GetUser() user: UserWithProfile
97+
): Promise<VerifyEmailCodeOutput> {
98+
const verified = await this.verificationService.verifyEmailCodeAuthenticated(user, {
99+
email: body.email,
100+
code: body.code,
101+
});
102+
103+
return {
104+
data: { verified },
105+
status: SUCCESS_STATUS,
106+
};
107+
}
108+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
2+
import { IsEmail, IsOptional, IsString } from "class-validator";
3+
4+
export class CheckEmailVerificationRequiredParams {
5+
@ApiProperty({ example: "user@example.com" })
6+
@IsEmail()
7+
email!: string;
8+
9+
@ApiPropertyOptional({ example: "user@example.com" })
10+
@IsOptional()
11+
@IsString()
12+
userSessionEmail?: string;
13+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
2+
import { IsEmail, IsOptional, IsString, IsBoolean } from "class-validator";
3+
4+
export class SendVerificationEmailInput {
5+
@ApiProperty({ example: "user@example.com" })
6+
@IsEmail()
7+
email!: string;
8+
9+
@ApiPropertyOptional({ example: "johndoe" })
10+
@IsOptional()
11+
@IsString()
12+
username?: string;
13+
14+
@ApiPropertyOptional({ example: "en" })
15+
@IsOptional()
16+
@IsString()
17+
language?: string;
18+
19+
@ApiPropertyOptional({ example: true })
20+
@IsOptional()
21+
@IsBoolean()
22+
isVerifyingEmail?: boolean;
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { IsEmail, IsString } from "class-validator";
3+
4+
export class VerifyEmailCodeInput {
5+
@ApiProperty({ example: "user@example.com" })
6+
@IsEmail()
7+
email!: string;
8+
9+
@ApiProperty({ example: "123456" })
10+
@IsString()
11+
code!: string;
12+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ApiProperty } from "@nestjs/swagger";
2+
import { Expose, Type } from "class-transformer";
3+
import { IsString, ValidateNested, IsBoolean } from "class-validator";
4+
5+
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
6+
7+
export class SendVerificationEmailData {
8+
@ApiProperty({ example: true })
9+
@IsBoolean()
10+
@Expose()
11+
readonly sent!: boolean;
12+
}
13+
14+
export class SendVerificationEmailOutput {
15+
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
16+
@IsString()
17+
@Expose()
18+
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
19+
20+
@ValidateNested()
21+
@Type(() => SendVerificationEmailData)
22+
@Expose()
23+
@ApiProperty({ type: SendVerificationEmailData })
24+
readonly data!: SendVerificationEmailData;
25+
}

0 commit comments

Comments
 (0)