Skip to content

fix: normalize email casing/whitespace across register and login - #57

Merged
projectamazonph merged 3 commits into
mainfrom
claude/fix-email-normalization
Aug 3, 2026
Merged

fix: normalize email casing/whitespace across register and login#57
projectamazonph merged 3 commits into
mainfrom
claude/fix-email-normalization

Conversation

@projectamazonph

@projectamazonph projectamazonph commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Follow-up to a Copilot review comment on fix: 14 bug-scan findings across persistence, engine, and API routes #55: registration validated email format but never normalized it, and src/lib/auth.ts's Credentials authorize() looked users up by exact string with no normalization either.
  • Postgres's @unique constraint on User.email is case-sensitive, so a user registering as Foo@Example.com could fail to log back in if they typed different casing/whitespace, and case-variant signups were treated as distinct accounts.
  • Added a shared normalizeEmail() helper (trim + lowercase) in src/lib/email.ts, used by both the registration route (validate + store) and auth.ts's login lookup, so both sides agree on the same canonical form.
  • Editing src/lib/auth.ts was 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 — clean
  • npm test — 630/630 passing (new tests for normalizeEmail and for register-route normalization/duplicate-detection behavior)
  • npm run build — succeeds

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Email addresses are now trimmed and treated consistently regardless of capitalization during registration and sign-in.
    • Registration prevents duplicate accounts when email addresses differ only by capitalization or surrounding spaces.
    • Existing accounts with mixed-case email addresses can still authenticate successfully.
  • Bug Fixes

    • Improved validation and handling of invalid, missing, or ambiguous sign-in credentials.
    • New accounts store normalized email addresses and use them consistently for account details.

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
Copilot AI review requested due to automatic review settings August 3, 2026 08:00
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
amazon-ad-console Ready Ready Preview Aug 3, 2026 8:32am

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@projectamazonph, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 83a72eaf-68e5-4251-8ab1-a0bc744ddef4

📥 Commits

Reviewing files that changed from the base of the PR and between f2f7ed9 and e71e2bb.

📒 Files selected for processing (3)
  • src/app/api/auth/register/__tests__/route.test.ts
  • src/app/api/auth/register/route.ts
  • src/lib/authorize-credentials.ts
📝 Walkthrough

Walkthrough

The 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.

Changes

Email authentication consistency

Layer / File(s) Summary
Shared email normalization
src/lib/email.ts, src/lib/__tests__/email.test.ts
Adds normalizeEmail to trim and lowercase addresses. Tests cover lowercasing, trimming, and idempotence.
Registration email handling
src/app/api/auth/register/route.ts, src/app/api/auth/register/__tests__/route.test.ts
Registration validates normalized emails, performs case-insensitive duplicate checks, stores normalized addresses, and rejects case variants of existing addresses.
Credential authorization extraction
src/lib/authorize-credentials.ts, src/lib/auth.ts, src/lib/__tests__/authorize-credentials.test.ts
Moves credential authorization into authorizeCredentials. The helper validates credentials, performs normalized lookups, rejects ambiguous matches, verifies passwords, and returns the authenticated user identity.

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
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes email normalization across registration and login, which is the main change.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-email-normalization

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 in src/lib/email.ts.
  • Updated login lookup in src/lib/auth.ts to 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.

Comment thread src/lib/auth.ts Outdated
Comment thread src/app/api/auth/register/route.ts Outdated
…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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between a53e2c6 and f2f7ed9.

📒 Files selected for processing (7)
  • src/app/api/auth/register/__tests__/route.test.ts
  • src/app/api/auth/register/route.ts
  • src/lib/__tests__/authorize-credentials.test.ts
  • src/lib/__tests__/email.test.ts
  • src/lib/auth.ts
  • src/lib/authorize-credentials.ts
  • src/lib/email.ts

Comment thread src/app/api/auth/register/route.ts
Comment thread src/lib/authorize-credentials.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
@projectamazonph
projectamazonph merged commit 2ff3e8c into main Aug 3, 2026
4 checks passed
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.

3 participants