fix: normalize email casing/whitespace across register and login - #57
Conversation
Postgres's unique constraint on User.email is case-sensitive, and login looked users up by exact string with no normalization — a user registering as "Foo@Example.com" could fail to log back in with different casing or incidental whitespace, and case-variant duplicate signups were treated as distinct accounts. Added a shared normalizeEmail() helper (trim + lowercase) used by both the registration route (validate + store) and auth.ts's Credentials authorize() (lookup), so both sides agree on the same canonical form. Editing src/lib/auth.ts done with explicit approval per this repo's protected-file convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jpq3N94psa27k8xASDy8B
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 41 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThe change adds shared email normalization, applies case-insensitive matching during registration and credential authorization, extracts authorization logic into a helper, and adds tests for normalization, duplicates, invalid credentials, ambiguous matches, and successful authentication. ChangesEmail authentication consistency
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CredentialsProvider
participant authorizeCredentials
participant Prisma
participant bcrypt
CredentialsProvider->>authorizeCredentials: Submit email and password
authorizeCredentials->>Prisma: Query normalized email
Prisma-->>authorizeCredentials: Matching user records
authorizeCredentials->>bcrypt: Compare password with hash
bcrypt-->>authorizeCredentials: Comparison result
authorizeCredentials-->>CredentialsProvider: User identity or null
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR introduces a shared email canonicalization helper and applies it to both the registration API route and NextAuth Credentials login flow to ensure consistent email matching/storage across auth paths.
Changes:
- Added
normalizeEmail()(trim + lowercase) as a shared helper insrc/lib/email.ts. - Updated login lookup in
src/lib/auth.tsto normalize the input email before querying. - Updated registration to validate/store the normalized email and added unit tests covering normalization + registration behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/email.ts | Adds shared email normalization helper used by auth flows. |
| src/lib/auth.ts | Normalizes credential email before user lookup during login. |
| src/lib/tests/email.test.ts | Adds unit tests for normalizeEmail(). |
| src/app/api/auth/register/route.ts | Normalizes email before validation, duplicate check, and persistence. |
| src/app/api/auth/register/tests/route.test.ts | Adds registration tests asserting normalization + duplicate behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…counts Follow-up to Copilot review comments on this PR: normalizing only the login query to lowercase would have broken login for any account already registered with a mixed-case email (their stored row wouldn't exact-match the now-lowercased lookup) — a regression the previous commit would have introduced. Switched login and registration's duplicate-check to a case- insensitive lookup (Prisma's `mode: 'insensitive'`) instead of an exact match, so legacy rows are found without a data migration. Login also fails closed (denies + logs) if a case-insensitive lookup somehow matches more than one row, rather than silently picking one account under the caller's identity. Also fixed a smaller related gap: `authorize()` cast `credentials.email` to `string` without checking it actually was one — NextAuth doesn't enforce that at runtime. Extracted the login logic into authorize-credentials.ts (with zero next-auth import) so it's unit testable — next-auth's own import chain pulls in next/server, which isn't available in the Vitest environment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jpq3N94psa27k8xASDy8B
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/api/auth/register/route.ts`:
- Around line 40-44: Add a nested try/catch around prisma.user.create in the
registration handler, detect Prisma error code P2002, and return the same 400
“User already exists” response used by the existing-user path; rethrow other
errors so the outer generic catch continues returning 500.
In `@src/lib/authorize-credentials.ts`:
- Around line 34-40: Update the multiple-match logging in the credential
authorization flow to remove normalizedEmail from the console.error payload,
while retaining the userIds needed to investigate the anomaly.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 13aae2f5-0204-48ef-bbc4-24df316e15d1
📒 Files selected for processing (7)
src/app/api/auth/register/__tests__/route.test.tssrc/app/api/auth/register/route.tssrc/lib/__tests__/authorize-credentials.test.tssrc/lib/__tests__/email.test.tssrc/lib/auth.tssrc/lib/authorize-credentials.tssrc/lib/email.ts
- Registration's findFirst duplicate-check and create() aren't atomic, so a concurrent registration with the same normalized email could slip past the check and hit the DB's unique constraint on create(), which the generic catch turned into an unhelpful 500 instead of the same 400 "User already exists" the check path already returns. Wrapped create() to catch Prisma's P2002 (unique constraint violation) specifically and return the same duplicate response; any other error still rethrows to the existing 500 handler. - Dropped the raw email address from the "multiple users matched case-insensitively" log line in authorize-credentials.ts — the userIds already let an engineer trace the anomaly in the DB, and PII doesn't need to also sit in log storage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Jpq3N94psa27k8xASDy8B
Summary
src/lib/auth.ts's Credentialsauthorize()looked users up by exact string with no normalization either.@uniqueconstraint onUser.emailis case-sensitive, so a user registering asFoo@Example.comcould fail to log back in if they typed different casing/whitespace, and case-variant signups were treated as distinct accounts.normalizeEmail()helper (trim + lowercase) insrc/lib/email.ts, used by both the registration route (validate + store) andauth.ts's login lookup, so both sides agree on the same canonical form.src/lib/auth.tswas done with explicit user approval, per this repo's protected-file convention (CLAUDE.md/gate.yaml/loop-constraints.md).Test plan
npm run type-check— cleannpm test— 630/630 passing (new tests fornormalizeEmailand for register-route normalization/duplicate-detection behavior)npm run build— succeedsGenerated by Claude Code
Summary by CodeRabbit
New Features
Bug Fixes