Skip to content

feat: drop email verification at signup, verify implicitly on use#2289

Merged
aalemayhu merged 2 commits into
mainfrom
feat/drop-email-verification
May 15, 2026
Merged

feat: drop email verification at signup, verify implicitly on use#2289
aalemayhu merged 2 commits into
mainfrom
feat/drop-email-verification

Conversation

@aalemayhu
Copy link
Copy Markdown
Contributor

@aalemayhu aalemayhu commented May 15, 2026

Summary

  • Email verification at signup was unreliable in the wild — Hotjar showed users landing on /login?verify_error=expired despite never clicking the link. Root cause: GET /api/users/verify/:token is single-use, and email security scanners (Outlook Safe Links, Mimecast, Gmail link checker, Slack/Discord previews) prefetch the URL on delivery and burn the token before the human ever clicks.
  • Trio reviewed (pm/designer/engineer): the email_verified column gates nothing functional today — not uploads, not conversion, not Notion, not Patreon, not Stripe, not magic-link login, not password reset. Removing it pays no defensive cost.
  • The proposal: drop the up-front verification email + banner; mark email_verified = true implicitly the first time a user completes a magic-link login or a password reset. Both are stronger proofs of mailbox control than a clicked link.

What changed

Server

  • register no longer creates a verify_email token or sends a verification email.
  • POST /api/users/resend-verification removed (route + controller + service method).
  • EmailService.sendVerificationEmail + VERIFY_EMAIL_TEMPLATE + templates/verify-email.html removed.
  • verifyMagicLink controller now calls markEmailVerified after a successful 'login' branch and after the 'password_reset' branch — implicit verification.
  • verifyEmail route kept alive so in-flight emails still work; success now redirects to /login?verified=1 (or /account?verified=1 if signed in), failure to /login?verify_error=expired (or /account?verify_error=expired if signed in).
  • users.email_verified column and 'verify_email' token type kept — no migration. Existing unverified users will be implicitly verified the next time they use a magic link or reset their password.

Web

  • EmailVerificationBanner component + tests + CSS module deleted; unwired from App.tsx, AppShell, SidebarLayout.
  • /account page: removed the verify prompt block and the resend button. Renders a quiet "Email verified" line under the email when email_verified === true.
  • Added a ?verified=1 success message to TopMessage so the success path on /login shows "Email verified. Sign in to continue."
  • Added a dismissible ?verified=1 toast on /account.
  • Removed Backend.resendVerificationEmail.

Tests

  • Deleted dead service tests (verify-email send + magic token storage + skipEmailVerification flag + entire resendVerificationEmail describe).
  • Deleted dead controller tests (entire resendVerificationEmail describe).
  • Updated verifyEmail redirects to /login?verified=1 and /account?verified=1.
  • Added implicit-verify coverage: verifyMagicLink with 'login' and 'password_reset' purposes both call markEmailVerified.

Changelog

  • Added a one-line entry to web/src/pages/WhatsNewPage/changelog.ts.

What did not change

  • users.email_verified column stays.
  • 'verify_email' token type stays.
  • magic_tokens table stays.
  • Magic-link login + password-reset flows are functionally unchanged — they just additionally mark email_verified = true.
  • No migration. Skipping the one-time email_verified = true backfill for existing unverified users — implicit-verify will catch them on next use, and nothing currently gates on the column anyway.

Diff stats

25 files changed, 144 insertions(+), 689 deletions(-)

Sonar

sonar-scanner is not installed locally so SonarCloud will run on push. No new HTTP sinks, no new HTML rendering, no new file paths derived from user input — taint surface is unchanged. Net change is heavy deletion.

Test plan

  • pnpm test passes for touched files (verified locally; full suite passes excluding the stale .claude/worktrees/ from earlier agent sessions).
  • pnpm typecheck passes (server).
  • pnpm --filter 2anki-web typecheck passes.
  • pnpm --filter 2anki-web test passes (66 files, 463 tests).
  • pnpm --filter 2anki-web lint passes.
  • Manual: register a new account → no verification email, no banner.
  • Manual: request a magic-link login → click the link → users.email_verified flips to true.
  • Manual: request a password reset → complete it → users.email_verified flips to true.
  • Manual: open an in-flight verification email URL (one issued before deploy) → still redirects with ?verified=1 and shows the success toast.
  • Manual: visit /account while verified → quiet "Email verified" line appears under the email.

🤖 Generated with Claude Code


View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

The verify-email-on-signup flow was unreliable in the wild — Hotjar showed
users landing on `/login?verify_error=expired` despite never having clicked
the link. Root cause: `GET /api/users/verify/:token` is single-use, and
email security scanners (Outlook Safe Links, Mimecast, Gmail link checker,
Slack/Discord previews) prefetch the URL on delivery, burning the token
before the human ever clicks.

Trio reviewed (pm/designer/engineer): the verification gate gates nothing
functional today (not uploads, conversion, Notion, Patreon, Stripe, magic-
link login, or password reset). Removing it pays no defensive cost. Account
recovery is self-verifying: a typo'd email never receives the password-
reset link.

Changes:
- Stop auto-sending the verification email at register
- Delete `EmailVerificationBanner` (the home-page nag) and unwire its props
- Delete the verify prompt + resend button on `/account`
- Render a quiet "Email verified" line on `/account` only when true
- Delete `POST /api/users/resend-verification`, the `Backend.resendVerificationEmail`
  client method, `sendVerificationEmail`, `VERIFY_EMAIL_TEMPLATE`,
  and `templates/verify-email.html`
- Implicit verify: mark `email_verified = true` after a successful magic-
  link login or password-reset completion (stronger signal than a clicked link)
- Keep `GET /api/users/verify/:token` alive for in-flight emails; redirect
  to `/login?verified=1` (or `/account?verified=1` when signed in) on success
- Add a success toast on `/login?verified=1` via the existing TopMessage
- Keep `users.email_verified` column and the `'verify_email'` token type;
  no migration. Existing unverified users will be implicitly verified the
  next time they use a magic link or reset their password — costs nothing
  since nothing gates on the column.

Sonar: scanner not installed locally; no new HTTP sinks, no new HTML
rendering, no new file paths from user input — taint surface unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aalemayhu aalemayhu marked this pull request as ready for review May 15, 2026 19:20
- verifyEmail: use truthy check on sessionUser instead of `!= null` ternary
  (S7735, lead with the positive)
- TopMessage: use native <output> element instead of <div role="status">
  for the verified-email banner

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@aalemayhu aalemayhu merged commit a1d2956 into main May 15, 2026
9 checks passed
@aalemayhu aalemayhu deleted the feat/drop-email-verification branch May 15, 2026 19:30
@sonarqubecloud
Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant