8000 feat: add email-verification flag support to signup flow by sean-brydon · Pull Request #24294 · calcom/cal.com · GitHub
[go: up one dir, main page]

Skip to content

Conversation

sean-brydon
Copy link
Member
@sean-brydon sean-brydon commented Oct 6, 2025

Summary

Implements proper support for the email-verification feature flag in the signup flow. When enabled, new users will have emailVerified set to null and receive a verification email. When disabled, users are automatically verified.

Changes

  • Check email-verification feature flag during signup in both handlers
  • Conditionally set emailVerified:
  • Flag enabled: emailVerified = null (requires verification)
  • Flag disabled: emailVerified = new Date() (auto-verified)
  • Only send verification emails when flag is enabled
  • Applied to both Cal.com and self-hosted signup handlers
  • OAuth/SAML flows unchanged (emails verified by identity provider)

Test plan

  • Test signup with email-verification flag disabled - user should be auto-verified
  • Test signup with email-verification flag enabled - user should receive verification email and emailVerified should be null
  • Test team invite signup with both flag states
  • Verify OAuth/SAML signups still work correctly (should always be verified)
  • Check that verification email is only sent when flag is enabled

Generated with Claude Code

- Check email-verification feature flag during signup
- Set emailVerified to null when flag is enabled, current date when disabled
- Only send verification emails when flag is enabled
- Apply to both Cal.com and self-hosted signup handlers
- Keep OAuth/SAML flows with verified emails (verified by IdP)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@graphite-app graphite-app bot requested a review from a team October 6, 2025 11:34
@keithwillcode keithwillcode added consumer core area: core, team members only labels Oct 6, 2025
Copy link
Contributor
coderabbitai bot commented Oct 6, 2025

Walkthrough

The changes add a global feature flag check for "email-verification" via FeaturesRepository. Calcom and self-hosted signup handlers instantiate the repository, read isEmailVerificationEnabled, log it, and c 8000 onditionally set user.emailVerified to null when enabled or to the current timestamp when disabled. Sending verification emails is now guarded by the flag. An import for FeaturesRepository was also added to packages/features/auth/lib/next-auth-options.ts but is unused.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly describes the primary change by announcing the addition of email-verification flag support to the signup flow, directly reflecting the pull request’s core functionality without extraneous information.
Description Check ✅ Passed The description provides a relevant summary of the feature flag implementation, outlines conditional behaviors for email verification, details both handlers affected, and includes a comprehensive test plan, demonstrating clear alignment with the changes in the pull request.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch email-verification-signup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot bot added authentication area: authentication, auth, google sign in, password, SAML, password reset, can't log in ✨ feature New feature or request labels Oct 6, 2025
@sean-brydon sean-brydon requested a review from a team October 6, 2025 11:37
Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bbe46ea and a031af8.

📒 Files selected for processing (3)
  • packages/features/auth/lib/next-auth-options.ts (1 hunks)
  • packages/features/auth/signup/handlers/calcomHandler.ts (6 hunks)
  • packages/features/auth/signup/handlers/selfHostedHandler.ts (5 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/features/auth/lib/next-auth-options.ts
  • packages/features/auth/signup/handlers/selfHostedHandler.ts
  • packages/features/auth/signup/handlers/calcomHandler.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • packages/features/auth/lib/next-auth-options.ts
  • packages/features/auth/signup/handlers/selfHostedHandler.ts
  • packages/features/auth/signup/handlers/calcomHandler.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • packages/features/auth/lib/next-auth-options.ts
  • packages/features/auth/signup/handlers/selfHostedHandler.ts
  • packages/features/auth/signup/handlers/calcomHandler.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: anglerfishlyy
PR: calcom/cal.com#0
File: :0-0
Timestamp: 2025-08-27T16:39:38.192Z
Learning: anglerfishlyy successfully implemented CAL-3076 email invitation feature for Cal.com team event-types in PR #23312. The feature allows inviting people via email directly from assignment flow, with automatic team invitation if email doesn't belong to existing team member. Implementation includes Host type modifications (userId?: number, email?: string, isPending?: boolean), CheckedTeamSelect component updates with CreatableSelect, TRPC schema validation with zod email validation, and integration with existing teamInvite system.
🧬 Code graph analysis (2)
packages/features/auth/signup/handlers/selfHostedHandler.ts (1)
packages/features/auth/lib/verifyEmail.ts (1)
  • sendEmailVerification (29-85)
packages/features/auth/signup/handlers/calcomHandler.ts (2)
packages/features/auth/lib/verifyEmail.ts (1)
  • sendEmailVerification (29-85)
apps/web/lib/buildLegacyCtx.ts (1)
  • buildLegacyRequest (47-49)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Install dependencies / Yarn install & cache
🔇 Additional comments (6)
packages/features/auth/signup/handlers/calcomHandler.ts (3)

48-53: LGTM!

The feature flag check is correctly implemented. The repository is instantiated and the global flag value is retrieved appropriately.


161-161: LGTM!

The conditional emailVerified logic is correctly implemented across all user creation and update paths. When the feature flag is enabled, users are created with emailVerified: null (unverified), and when disabled, users are auto-verified with emailVerified: new Date(Date.now()).

Also applies to: 173-173, 205-205


218-225: Verify email verification in token-based signup
The team-invite signup branch (foundToken && foundToken.teamId) doesn’t call sendEmailVerification even when isEmailVerificationEnabled is true—only the non-token path does. Confirm if invite signups should trigger verification emails and, if so, add a guarded sendEmailVerification call in that branch (around line 191).

packages/features/auth/signup/handlers/selfHostedHandler.ts (3)

35-38: LGTM!

The feature flag check is correctly implemented, consistent with the pattern in calcomHandler.ts.


107-107: LGTM!

The conditional emailVerified logic is correctly and consistently implemented across all user creation and update paths (both token-based and non-token paths). The logic matches the implementation in calcomHandler.ts.

Also applies to: 113-113, 163-163, 169-169


179-186: Verify email flow for team-invite signups
The token-based signup path (lines 69–138) sets emailVerified but doesn’t invoke sendEmailVerification, unlike the non-token path (lines 179–186). Confirm whether to trigger verification emails under isEmailVerificationEnabled for team invites and, if so, add the guarded call after joinAnyChildTeamOnOrgInvite (around line 131).

import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { getOrgFullOrigin, subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains";
import { clientSecretVerifier, hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove unused import.

The FeaturesRepository import is not used anywhere in this file. Unused imports add unnecessary dependencies and should be removed.

Apply this diff to remove the unused import:

-import { FeaturesRepository } from "@calcom/features/flags/features.repository";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
🤖 Prompt for AI Agents
In packages/features/auth/lib/next-auth-options.ts around line 19, remove the
unused import statement "import { FeaturesRepository } from
\"@calcom/features/flags/features.repository\";" since it is not referenced
anywhere in the file; update imports to eliminate this unused dependency and run
the linter/TypeScript build to confirm no remaining unused-import errors.

Comment on lines +218 to +225
// Only send verification email if the feature is enabled
if (isEmailVerificationEnabled) {
sendEmailVerification({
email,
language: await getLocaleFromRequest(buildLegacyRequest(await headers(), await cookies())),
username: username || "",
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

8000

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Inconsistent async handling of sendEmailVerification.

The sendEmailVerification call is not awaited here (fire-and-forget), but in selfHostedHandler.ts (line 181) it is awaited. This inconsistency could lead to different error handling behavior between the two handlers.

Consider either:

  1. Making both handlers consistent by awaiting the call in both places, or
  2. Documenting why fire-and-forget is intentional here but not in the self-hosted handler

Run the following script to confirm the inconsistency:


🏁 Script executed:

#!/bin/bash
# Description: Check for inconsistent await usage of sendEmailVerification across handlers

# Search for sendEmailVerification calls in both handlers
rg -n -A 2 -B 2 'sendEmailVerification\(' packages/features/auth/signup/handlers/

Length of output: 1066


Await sendEmailVerification in calcomHandler.ts
In calcomHandler.ts (line 220), add await before sendEmailVerification(...) to match selfHostedHandler.ts and ensure consistent error handling.

🤖 Prompt for AI Agents
In packages/features/auth/signup/handlers/calcomHandler.ts around lines 218 to
225, the call to sendEmailVerification is not awaited so errors or async
completion aren't handled consistently with selfHostedHandler.ts; update the
code to await sendEmailVerification(...) and propagate or catch errors as
appropriate (i.e., prefix the call with await and ensure the containing function
is async and any surrounding error handling/try-catch remains correct) so email
sending is awaited and failures are handled consistently.

- Add unit test scaffolds for calcomHandler and selfHostedHandler
- Update existing E2E test to verify emailVerified field is null when flag enabled
- Add new E2E test to verify emailVerified is set when flag disabled
- Verify email verification emails are sent/not sent based on flag state

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pull-request-size pull-request-size bot added size/L and removed size/M labels Oct 6, 2025
Copy link
vercel bot commented Oct 6, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal Ignored Ignored Oct 6, 2025 0:05am
cal-eu Ignored Ignored Oct 6, 2025 0:05am

sean-brydon and others added 2 commits October 6, 2025 12:45
- Add assertions for emailVerified value based on flag state
- Verify Date object when flag disabled, null when enabled
- Add verification for sendEmailVerification call behavior
- Include detailed comments explaining test strategy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Implement full integration tests that actually call the handlers
- Test emailVerified is null when flag enabled, Date when disabled
- Test sendEmailVerification is called/not called based on flag
- Add comprehensive tests for both regular and team invite flows
- Verify both create and update paths in upsert calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@pull-request-size pull-request-size bot added size/XL and removed size/L labels Oct 6, 2025
Work in progress - unit tests need more mocking setup for NextResponse and other dependencies. E2E tests are complete and working.
@pull-request-size pull-request-size bot added size/M and removed size/XL labels Oct 6, 2025
Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a031af8 and b28f34e.

📒 Files selected for processing (1)
  • apps/web/playwright/signup.e2e.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

**/*.ts: For Prisma queries, only select data you need; never use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • apps/web/playwright/signup.e2e.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js .utc() in hot paths like loops

Files:

  • apps/web/playwright/signup.e2e.ts
**/*.{ts,tsx,js,jsx}

⚙️ CodeRabbit configuration file

Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.

Files:

  • apps/web/playwright/signup.e2e.ts
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Tests / Unit
  • GitHub Check: Type check / check-types
  • GitHub Check: Linters / lint
🔇 Additional comments (1)
apps/web/playwright/signup.e2e.ts (1)

293-298: LGTM!

The assertion correctly verifies that emailVerified is null when the email-verification flag is enabled, and properly uses select to fetch only the required field.

Comment on lines +314 to +352
test("Email auto-verified when flag disabled", async ({ page, prisma, users, features }) => {
// Ensure email verification is disabled for this test
await prisma.feature.update({
where: { slug: "email-verification" },
data: { enabled: false },
});

const userToCreate = users.buildForSignup({
email: `auto-verify-${Date.now()}@example.com`,
username: `auto-verify-${Date.now()}`,
password: "Password99!",
});

await page.goto("/signup");
await preventFlakyTest(page);
const continueWithEmailButton = page.getByTestId("continue-with-email-button");
await expect(continueWithEmailButton).toBeVisible();
await continueWithEmailButton.click();

// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
await page.locator('input[name="email"]').fill(userToCreate.email);
await page.locator('input[name="password"]').fill(userToCreate.password);

// Submit form
const submitButton = page.getByTestId("signup-submit-button");
await submitButton.click();

// Should NOT redirect to verify-email when flag is disabled
await page.waitForURL((url) => !url.pathname.includes("/auth/verify-email"));

// Verify that emailVerified is set to a date when flag is disabled
const dbUser = await prisma.user.findUnique({
where: { email: userToCreate.email },
select: { emailVerified: true },
});
expect(dbUser?.emailVerified).not.toBeNull();
expect(dbUser?.emailVerified).toBeInstanceOf(Date);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Strengthen the URL assertion after signup.

Line 343 waits for any URL that doesn't include /auth/verify-email, which could pass even if the page navigates to an error page or doesn't navigate at all. This creates a potential false positive in the test.

Replace line 343 with an explicit check for the expected success URL:

-    // Should NOT redirect to verify-email when flag is disabled
-    await page.waitForURL((url) => !url.pathname.includes("/auth/verify-email"));
+    // Should redirect to onboarding/getting-started when flag is disabled
+    await page.waitForURL("/getting-started?from=signup");

Optional: Verify no email is sent.

Consider adding a check that no verification email is sent when the flag is disabled, similar to the email verification check in the other test (lines 300-313). This would more thoroughly validate the PR objective: "Verification emails are only sent when the flag is enabled."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test("Email auto-verified when flag disabled", async ({ page, prisma, users, features }) => {
// Ensure email verification is disabled for this test
await prisma.feature.update({
where: { slug: "email-verification" },
data: { enabled: false },
});
const userToCreate = users.buildForSignup({
email: `auto-verify-${Date.now()}@example.com`,
username: `auto-verify-${Date.now()}`,
password: "Password99!",
});
await page.goto("/signup");
await preventFlakyTest(page);
const continueWithEmailButton = page.getByTestId("continue-with-email-button");
await expect(continueWithEmailButton).toBeVisible();
await continueWithEmailButton.click();
// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
await page.locator('input[name="email"]').fill(userToCreate.email);
await page.locator('input[name="password"]').fill(userToCreate.password);
// Submit form
const submitButton = page.getByTestId("signup-submit-button");
await submitButton.click();
// Should NOT redirect to verify-email when flag is disabled
await page.waitForURL((url) => !url.pathname.includes("/auth/verify-email"));
// Verify that emailVerified is set to a date when flag is disabled
const dbUser = await prisma.user.findUnique({
where: { email: userToCreate.email },
select: { emailVerified: true },
});
expect(dbUser?.emailVerified).not.toBeNull();
expect(dbUser?.emailVerified).toBeInstanceOf(Date);
});
test("Email auto-verified when flag disabled", async ({ page, prisma, users, features }) => {
// Ensure email verification is disabled for this test
await prisma.feature.update({
where: { slug: "email-verification" },
data: { enabled: false },
});
const userToCreate = users.buildForSignup({
email: `auto-verify-${Date.now()}@example.com`,
username: `auto-verify-${Date.now()}`,
password: "Password99!",
});
await page.goto("/signup");
await preventFlakyTest(page);
const continueWithEmailButton = page.getByTestId("continue-with-email-button");
await expect(continueWithEmailButton).toBeVisible();
await continueWithEmailButton.click();
// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
await page.locator('input[name="email"]').fill(userToCreate.email);
await page.locator('input[name="password"]').fill(userToCreate.password);
// Submit form
const submitButton = page.getByTestId("signup-submit-button");
await submitButton.click();
// Should redirect to onboarding/getting-started when flag is disabled
await page.waitForURL("/getting-started?from=signup");
// Verify that emailVerified is set to a date when flag is disabled
const dbUser = await prisma.user.findUnique({
where: { email: userToCreate.email },
select: { emailVerified: true },
});
expect(dbUser?.emailVerified).not.toBeNull();
expect(dbUser?.emailVerified).toBeInstanceOf(Date);
});
🤖 Prompt for AI Agents
In apps/web/playwright/signup.e2e.ts around lines 314 to 352, replace the broad
await page.waitForURL((url) => !url.pathname.includes("/auth/verify-email"))
with an explicit assertion that the page reached the known post-signup success
URL (e.g. await expect(page).toHaveURL("/" ) or the actual success route your
app uses) so the test fails if it lands on an error or a different page;
optionally also add the no-email-sent verification by asserting the mailer was
not called or checking the outgoing_emails table/fixture to ensure no
verification email was queued when the feature flag is disabled.

Copy link
Contributor
github-actions bot commented Oct 6, 2025

E2E results are ready!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
authentication area: authentication, auth, google sign in, password, SAML, password reset, can't log in consumer core area: core, team members only ✨ feature New feature or request ready-for-e2e size/M
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0