Skip to content

feat: implement guest invite confirmation#24684

Open
husniabad wants to merge 8 commits intocalcom:mainfrom
husniabad:feat/guest-confirmation-flow
Open

feat: implement guest invite confirmation#24684
husniabad wants to merge 8 commits intocalcom:mainfrom
husniabad:feat/guest-confirmation-flow

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 guest email confirmation flow (CAL-6586). Guests verify via a link before being added; calendars and notifications update after confirmation, with safe locale fallback and time zone–aware dates.

  • New Features

    • Added PendingGuest model with unique token and 48h expiry.
    • Sends verification emails (new template + i18n) with time zone–aware dates; confirm via /api/guest-verification/confirm with localized success/error pages and safe locale fallback (/guest-verification/success, /guest-verification/error).
    • Booking service defers guests needing verification, upserts pending records, and sends emails after booking creation.
    • Confirmation validates token/email, active booking, and organizer ID; atomically claims the pending guest and adds the attendee, updates calendar attendees using scoped credentials (incl. service account key), and sends emails.
    • Cleanup endpoint /api/cron/cleanup-pending-guests runs every 6h; authorized via CRON_API_KEY or CRON_SECRET.
  • Migration

    • Run Prisma migrations.
    • Set EMAIL_FROM and CRON_API_KEY or CRON_SECRET.

Written for commit 7708723. Summary will update on new commits.


Open with Devin

@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?

@github-actions
Copy link
Contributor

github-actions bot commented Nov 4, 2025

This PR has been marked as stale due to inactivity. If you're still working on it or need any help, please let us know or update the PR to keep it active.

@github-actions
Copy link
Contributor

Devin AI is resolving merge conflicts

This PR has merge conflicts with the main branch. A Devin session has been created to automatically resolve them.

View Devin Session

Devin will:

  1. Merge the latest main into this branch
  2. Resolve any conflicts intelligently
  3. Run lint/type checks to ensure validity
  4. Push the resolved changes

If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.

@github-actions
Copy link
Contributor

Devin AI is resolving merge conflicts

This PR has merge conflicts with the main branch. A Devin session has been created to automatically resolve them.

View Devin Session

Devin will:

  1. Merge the latest main into this branch
  2. Resolve any conflicts intelligently
  3. Run lint/type checks to ensure validity
  4. Push the resolved changes

If you prefer to resolve conflicts manually, you can close the Devin session and handle it yourself.

@devin-ai-integration
Copy link
Contributor

Merge Conflict Resolution - Push Authentication Failed

I resolved all merge conflicts between feat/guest-confirmation-flow and upstream/main, but was unable to push the changes to the fork repository due to authentication failure with the provided PAT token.

Conflicts Resolved

  1. packages/emails/src/templates/index.ts - Added GuestVerificationEmail export alongside new upstream exports (OrganizationCreationEmail, OrganizerAddAttendeeEmail, etc.)

  2. packages/features/bookings/lib/service/RegularBookingService.ts (2 conflicts):

    • Imports: Kept both PR's createPendingGuestsAndSendEmails import and upstream's isWithinMinimumRescheduleNotice import
    • Handler body: Kept both PR's pending guest verification email logic and upstream's async tasker logic
  3. packages/prisma/schema.prisma (2 conflicts):

    • Booking model: Added both pendingGuests (PR) and wrongAssignmentReports/routingTrace (upstream) relations
    • End of file: Kept PendingGuest model (PR) and new upstream models (IntegrationAttributeSync, AttributeSyncRule, AttributeSyncFieldMapping, PendingRoutingTrace, RoutingTrace)

Additional Type Fixes

  • Fixed import { renderEmail } from "../"import renderEmail from "../src/renderEmail" in guest-verification-email.ts (matching codebase convention)
  • Fixed log.info/log.errortracingLogger.info/tracingLogger.error in RegularBookingService.ts (matching the logger used in the rest of the handler)

Validation

  • Merge commit shows exactly 14 files changed (matching the original PR)
  • +580 -1 line changes match the original PR

Pre-existing Type Error (not from merge)

  • apps/web/pages/api/guest-verification/confirm.ts imports sendAddGuestsEmails from @calcom/emails, but this function is only exported from @calcom/emails/email-manager.ts, not from the package barrel. This issue exists in the original PR code.

Authentication Error

remote: Invalid username or token. Password authentication is not supported for Git operations.
fatal: Authentication failed for 'https://github.com/husniabad/cal.com.git/'

The PR author will need to pull these changes or resolve the conflicts manually.

Link to Devin run

@github-actions github-actions bot added the 🧹 Improvements Improvements to existing features. Mostly UX/UI label Feb 23, 2026
devin-ai-integration[bot]

This comment was marked as resolved.

sahitya-chandra and others added 2 commits February 23, 2026 16:51
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
… handling duplicate attendee creation atomically
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

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 devin-conflict-resolution emails area: emails, cancellation email, reschedule email, inbox, spam folder, not getting email ✨ feature New feature or request 🧹 Improvements Improvements to existing features. Mostly UX/UI 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

4 participants