Skip to content

Conversation

@husniabad
Copy link
Contributor

@husniabad husniabad commented Oct 25, 2025

What does this PR do?

This PR implements a complete guest email verification flow for the impersonation prevention feature introduced in PR #24298. Previously, when a user with requiresBookerEmailVerification enabled was added as a guest to a booking, they were silently filtered out with no notification. This PR changes that behavior by:

  • Adding a PendingGuest database model to track guests awaiting verification
  • Sending verification emails with secure tokens to guests requiring verification
  • Providing API endpoints and UI pages for the verification flow
  • Automatically adding verified guests to bookings and notifying all parties
  • Implementing automated cleanup of expired pending guests via cron job

  1. Before: Guest with requiresBookerEmailVerification=true is silently filtered out when added to booking
  2. After:
    • Guest receives verification email with button/link
    • Clicking link verifies ownership and adds them to the booking
    • Guest receives booking confirmation with meeting details
    • Organizer is notified that guest was added
    • Invalid/expired tokens show appropriate error pages

Visual Demo (For contributors especially)

A visual demonstration is strongly recommended, for both the original and new change (video / image - any one).

Video Demo (if applicable):

  • Show screen recordings of the issue or feature.
  • Demonstrate how to reproduce the issue, the behavior before and after the change.

Image Demo (if applicable):

  1. Create booking and add guest:
    guest-booking

  2. Created booking without the guest:
    guest-booking-before

  3. Receive guest invite request:
    guest-invite-request

  4. Confirm the invitation:
    guest-success-verify

  5. Receive guest attending email notification:
    guest-confirm-email
    guest-organizer-invite-confirm-inform

  6. Booking updated:
    guest-booking-after

  7. Link expired or invalid token:
    guest-error-verify

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Happy Path - Email Verification:

  1. As User A, create a booking and add User B's email as a guest
  2. Verify User B does NOT appear in the attendees list immediately
  3. Check User B's email inbox for verification email
  4. Click "Verify Email" button in the email
  5. Verify redirect to success page
  6. Check that User B now appears in booking attendees
  7. Verify User B receives booking confirmation email with meeting details
  8. Verify User A receives "New guests added" notification

Error Cases:

  1. Expired Token: Wait 48+ hours, click verification link → Should show error page
  2. Invalid Token: Manually modify token in URL → Should show error page
  3. Cancelled Booking: Cancel booking, then click verification link → Should show error page
  4. Already Verified: Click verification link twice → Second click shows error

Cleanup Job:

  1. Create pending guests with past expiration dates
  2. Trigger cron job: curl http://localhost:3000/api/cron/cleanup-pending-guests
  3. Verify expired pending guests are deleted from database
  • Are there environment variables that should be set?
  • What are the minimal test data to have?
  • What is expected (happy path) to have (input and output)?
  • Any other important info that could help to test that PR

Checklist

  • I haven't read the contributing guide
  • My code doesn't follow the style guidelines of this project
  • I haven't commented my code, particularly in hard-to-understand areas
  • I haven't checked if my changes generate no new warnings

Summary by cubic

Implements a full guest email verification flow to prevent impersonation. Guests who require verification now confirm via a secure link before being added to a booking, and all parties are notified. Addresses CAL-6586.

  • New Features

    • Added PendingGuest model with unique token and 48h expiry.
    • Sends verification email with secure link; adds success/error pages.
    • Confirm endpoint validates token, adds guest to booking, updates calendar attendees, and emails guest + organizer.
    • Booking service defers guests requiring verification and triggers emails instead of silently dropping them.
    • Cleanup cron endpoint and schedule to remove expired pending guests.
    • Added i18n strings for the new email.
  • Migration

    • Run Prisma migrations.
    • Set EMAIL_FROM and either CRON_API_KEY or CRON_SECRET for /api/cron/cleanup-pending-guests (scheduled every 6h).

@vercel
Copy link

vercel bot commented Oct 25, 2025

@husniabad is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions bot added api area: API, enterprise API, access token, OAuth consumer emails area: emails, cancellation email, reschedule email, inbox, spam folder, not getting email Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request ❗️ migrations contains migration files labels Oct 25, 2025
@husniabad husniabad marked this pull request as ready for review October 25, 2025 07:26
@husniabad husniabad requested a review from a team as a code owner October 25, 2025 07:26
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Oct 25, 2025
@graphite-app graphite-app bot requested a review from a team October 25, 2025 07:27
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 14 files

Prompt for AI agents (all 7 issues)

Understand the root cause of the following 7 issues and fix them.


<file name="apps/web/pages/guest-verification/error.tsx">

<violation number="1" location="apps/web/pages/guest-verification/error.tsx:10">
Please wrap these user-facing strings with the t() translation helper instead of hardcoding English text so the new guest verification error view remains localized.</violation>
</file>

<file name="packages/prisma/migrations/20251025001217_implement_guest_confirmation_flow/migration.sql">

<violation number="1" location="packages/prisma/migrations/20251025001217_implement_guest_confirmation_flow/migration.sql:24">
The non-unique index on token is redundant because the preceding unique index already provides the same B-tree structure; this wastes storage and slows writes without improving reads.</violation>
</file>

<file name="apps/web/pages/guest-verification/success.tsx">

<violation number="1" location="apps/web/pages/guest-verification/success.tsx:8">
Please wrap the success-page copy in the localization helper instead of hardcoding strings so translations can be applied consistently across the UI.</violation>
</file>

<file name="packages/features/bookings/lib/handlePendingGuests.ts">

<violation number="1" location="packages/features/bookings/lib/handlePendingGuests.ts:24">
Recreating a pending guest unconditionally will throw once a pending record already exists because PendingGuest has a unique (email, bookingId) constraint. Handle the existing record (e.g., update or upsert it) instead of always calling create.</violation>
</file>

<file name="apps/web/pages/api/guest-verification/confirm.ts">

<violation number="1" location="apps/web/pages/api/guest-verification/confirm.ts:35">
Switch this Prisma lookup to use `select` instead of `include`; we only need the booking metadata (status/userId/bookingId), but the current include pulls every attendee record unnecessarily, increasing query cost and widening data exposure.</violation>
</file>

<file name="packages/prisma/schema.prisma">

<violation number="1" location="packages/prisma/schema.prisma:2754">
Rule violated: **Prevent Direct NOW() Usage in Database Queries**

Replace now() with a timezone-aware default to comply with the Prevent Direct NOW() Usage in Database Queries rule.</violation>
</file>

<file name="packages/features/bookings/lib/service/RegularBookingService.ts">

<violation number="1" location="packages/features/bookings/lib/service/RegularBookingService.ts:2607">
Rule violated: **Avoid Logging Sensitive Information**

Please avoid logging guest email addresses; logging the guestsToVerify array exposes PII and violates the Avoid Logging Sensitive Information policy.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

const getErrorMessage = () => {
switch (reason) {
case "expired":
return "The verification link has expired. Please contact the meeting organizer.";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap these user-facing strings with the t() translation helper instead of hardcoding English text so the new guest verification error view remains localized.

Prompt for AI agents
Address the following comment on apps/web/pages/guest-verification/error.tsx at line 10:

<comment>Please wrap these user-facing strings with the t() translation helper instead of hardcoding English text so the new guest verification error view remains localized.</comment>

<file context>
@@ -0,0 +1,34 @@
+  const getErrorMessage = () =&gt; {
+    switch (reason) {
+      case &quot;expired&quot;:
+        return &quot;The verification link has expired. Please contact the meeting organizer.&quot;;
+      case &quot;invalid&quot;:
+        return &quot;The verification link is invalid. Please check your email for the correct link.&quot;;
</file context>
Fix with Cubic

CREATE INDEX "PendingGuest_bookingId_idx" ON "public"."PendingGuest"("bookingId");

-- CreateIndex
CREATE INDEX "PendingGuest_token_idx" ON "public"."PendingGuest"("token");
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-unique index on token is redundant because the preceding unique index already provides the same B-tree structure; this wastes storage and slows writes without improving reads.

Prompt for AI agents
Address the following comment on packages/prisma/migrations/20251025001217_implement_guest_confirmation_flow/migration.sql at line 24:

<comment>The non-unique index on token is redundant because the preceding unique index already provides the same B-tree structure; this wastes storage and slows writes without improving reads.</comment>

<file context>
@@ -0,0 +1,33 @@
+CREATE INDEX &quot;PendingGuest_bookingId_idx&quot; ON &quot;public&quot;.&quot;PendingGuest&quot;(&quot;bookingId&quot;);
+
+-- CreateIndex
+CREATE INDEX &quot;PendingGuest_token_idx&quot; ON &quot;public&quot;.&quot;PendingGuest&quot;(&quot;token&quot;);
+
+-- CreateIndex
</file context>

✅ Addressed in c22181a

<svg className="mx-auto mb-4 h-16 w-16 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 className="mb-2 text-2xl font-bold text-gray-900">Email Verified!</h1>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please wrap the success-page copy in the localization helper instead of hardcoding strings so translations can be applied consistently across the UI.

Prompt for AI agents
Address the following comment on apps/web/pages/guest-verification/success.tsx at line 8:

<comment>Please wrap the success-page copy in the localization helper instead of hardcoding strings so translations can be applied consistently across the UI.</comment>

<file context>
@@ -0,0 +1,15 @@
+        &lt;svg className=&quot;mx-auto mb-4 h-16 w-16 text-green-500&quot; fill=&quot;none&quot; viewBox=&quot;0 0 24 24&quot; stroke=&quot;currentColor&quot;&gt;
+          &lt;path strokeLinecap=&quot;round&quot; strokeLinejoin=&quot;round&quot; strokeWidth={2} d=&quot;M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z&quot; /&gt;
+        &lt;/svg&gt;
+        &lt;h1 className=&quot;mb-2 text-2xl font-bold text-gray-900&quot;&gt;Email Verified!&lt;/h1&gt;
+        &lt;p className=&quot;text-gray-600&quot;&gt;
+          You&amp;apos;ve been successfully added to the meeting. Check your email for the meeting details.
</file context>

✅ Addressed in c22181a

const token = generateToken();
const expiresAt = dayjs().add(48, "hours").toDate();

await prisma.pendingGuest.create({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recreating a pending guest unconditionally will throw once a pending record already exists because PendingGuest has a unique (email, bookingId) constraint. Handle the existing record (e.g., update or upsert it) instead of always calling create.

Prompt for AI agents
Address the following comment on packages/features/bookings/lib/handlePendingGuests.ts at line 24:

<comment>Recreating a pending guest unconditionally will throw once a pending record already exists because PendingGuest has a unique (email, bookingId) constraint. Handle the existing record (e.g., update or upsert it) instead of always calling create.</comment>

<file context>
@@ -0,0 +1,51 @@
+    const token = generateToken();
+    const expiresAt = dayjs().add(48, &quot;hours&quot;).toDate();
+
+    await prisma.pendingGuest.create({
+      data: {
+        email: guestEmail,
</file context>

✅ Addressed in c22181a

expiresAt: { gte: new Date() },
},
include: {
booking: {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch this Prisma lookup to use select instead of include; we only need the booking metadata (status/userId/bookingId), but the current include pulls every attendee record unnecessarily, increasing query cost and widening data exposure.

Prompt for AI agents
Address the following comment on apps/web/pages/api/guest-verification/confirm.ts at line 35:

<comment>Switch this Prisma lookup to use `select` instead of `include`; we only need the booking metadata (status/userId/bookingId), but the current include pulls every attendee record unnecessarily, increasing query cost and widening data exposure.</comment>

<file context>
@@ -0,0 +1,163 @@
+        expiresAt: { gte: new Date() },
+      },
+      include: {
+        booking: {
+          include: {
+            attendees: true,
</file context>

✅ Addressed in c22181a

token String @unique
verified Boolean @default(false)
expiresAt DateTime
createdAt DateTime @default(now())
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Prevent Direct NOW() Usage in Database Queries

Replace now() with a timezone-aware default to comply with the Prevent Direct NOW() Usage in Database Queries rule.

Prompt for AI agents
Address the following comment on packages/prisma/schema.prisma at line 2754:

<comment>Replace now() with a timezone-aware default to comply with the Prevent Direct NOW() Usage in Database Queries rule.</comment>

<file context>
@@ -2741,3 +2742,20 @@ model CalendarCacheEvent {
+  token     String   @unique
+  verified  Boolean  @default(false)
+  expiresAt DateTime
+  createdAt DateTime @default(now())
+
+  @@unique([email, bookingId])
</file context>
Suggested change
createdAt DateTime @default(now())
createdAt DateTime @default(dbgenerated("now() AT TIME ZONE 'UTC'"))
Fix with Cubic

},
bookerUrl,
});
log.info("Sent verification emails to pending guests", guestsToVerify);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rule violated: Avoid Logging Sensitive Information

Please avoid logging guest email addresses; logging the guestsToVerify array exposes PII and violates the Avoid Logging Sensitive Information policy.

Prompt for AI agents
Address the following comment on packages/features/bookings/lib/service/RegularBookingService.ts at line 2607:

<comment>Please avoid logging guest email addresses; logging the guestsToVerify array exposes PII and violates the Avoid Logging Sensitive Information policy.</comment>

<file context>
@@ -2588,6 +2590,25 @@ async function handler(
+          },
+          bookerUrl,
+        });
+        log.info(&quot;Sent verification emails to pending guests&quot;, guestsToVerify);
+      } catch (error) {
+        log.error(&quot;Error sending verification emails to pending guests&quot;, error);
</file context>
Suggested change
log.info("Sent verification emails to pending guests", guestsToVerify);
log.info("Sent verification emails to pending guests", { count: guestsToVerify.length });

✅ Addressed in c22181a

@anikdhabal
Copy link
Contributor

anikdhabal commented Oct 27, 2025

@husniabad This issue is assigned to somebody. Have you got confirmation from them before working on it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api area: API, enterprise API, access token, OAuth community Created by Linear-GitHub Sync consumer emails area: emails, cancellation email, reschedule email, inbox, spam folder, not getting email ✨ feature New feature or request Medium priority Created by Linear-GitHub Sync ❗️ migrations contains migration files size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Properly implement a guest confirmation flow for new impersonation prevention setting

2 participants