feat: drop email verification at signup, verify implicitly on use#2289
Merged
Conversation
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>
- 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>
This was referenced May 15, 2026
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
/login?verify_error=expireddespite never clicking the link. Root cause:GET /api/users/verify/:tokenis 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.email_verifiedcolumn 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.email_verified = trueimplicitly 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
registerno longer creates averify_emailtoken or sends a verification email.POST /api/users/resend-verificationremoved (route + controller + service method).EmailService.sendVerificationEmail+VERIFY_EMAIL_TEMPLATE+templates/verify-email.htmlremoved.verifyMagicLinkcontroller now callsmarkEmailVerifiedafter a successful'login'branch and after the'password_reset'branch — implicit verification.verifyEmailroute kept alive so in-flight emails still work; success now redirects to/login?verified=1(or/account?verified=1if signed in), failure to/login?verify_error=expired(or/account?verify_error=expiredif signed in).users.email_verifiedcolumn 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
EmailVerificationBannercomponent + tests + CSS module deleted; unwired fromApp.tsx,AppShell,SidebarLayout./accountpage: removed the verify prompt block and the resend button. Renders a quiet "Email verified" line under the email whenemail_verified === true.?verified=1success message toTopMessageso the success path on/loginshows "Email verified. Sign in to continue."?verified=1toast on/account.Backend.resendVerificationEmail.Tests
resendVerificationEmaildescribe).resendVerificationEmaildescribe).verifyEmailredirects to/login?verified=1and/account?verified=1.verifyMagicLinkwith'login'and'password_reset'purposes both callmarkEmailVerified.Changelog
web/src/pages/WhatsNewPage/changelog.ts.What did not change
users.email_verifiedcolumn stays.'verify_email'token type stays.magic_tokenstable stays.email_verified = true.email_verified = truebackfill 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-scanneris 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 testpasses for touched files (verified locally; full suite passes excluding the stale.claude/worktrees/from earlier agent sessions).pnpm typecheckpasses (server).pnpm --filter 2anki-web typecheckpasses.pnpm --filter 2anki-web testpasses (66 files, 463 tests).pnpm --filter 2anki-web lintpasses.users.email_verifiedflips totrue.users.email_verifiedflips totrue.?verified=1and shows the success toast./accountwhile verified → quiet "Email verified" line appears under the email.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmithwith what you need.