fix: normalize email to lowercase on register and login - #322
Merged
EmmanuelOchaje merged 4 commits intoSep 30, 2026
Merged
Conversation
@isemail() validates an address but leaves its casing and surrounding whitespace untouched, so `Ada@Example.com` and `ada@example.com` are treated as different emails. The auth service passes dto.email straight into both the dedupe findUnique and the create, and the User.email @unique constraint in Postgres is case-sensitive. The same person can register twice under different casing, and someone who registered as `Ada@Example.com` is locked out when they log in as `ada@example.com`. Leading/trailing whitespace has the same effect, and a padded address fails @isemail() outright with a 400. Add a class-transformer @Transform to the email field that trims and lowercases the value before validation. ValidationPipe runs with transform: true, so the service and the unique constraint now always see one canonical address. This matches the DTO style already in the repo, which uses class-transformer (@type in create-event.dto.ts). Apply the same normalization to the users lookup query, which had the identical defect: GET /users/lookup?email=User@x.com would not resolve an account stored as user@x.com. Move the inline LookupQuery class into users/dto/lookup-query.dto.ts so it sits with the other DTOs and can be unit-tested the same way. The transform keeps a typeof guard so a non-string email still falls through to @isemail() instead of throwing.
✅ Deploy Preview for stellarticketsbackend ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…lize-email # Conflicts: # src/users/users.controller.ts
…lize-email # Conflicts: # src/auth/dto/register.dto.ts
email had no initializer and no definite assignment assertion, so `tsc --noEmit` fails under strict mode (TS2564). login.dto.ts and register.dto.ts already use the `!` form; match it here.
Contributor
Author
|
Checking in on this one: the branch still merges cleanly and the deploy preview passes. Happy to change anything if needed. |
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.
Fixes #47
Right now
@IsEmail()validates an address but leaves its casing andsurrounding whitespace alone. The auth service passes
dto.emailstraight intoboth the dedupe
findUniqueand thecreate, and theUser.email @uniqueconstraint in Postgres is case-sensitive. So
Ada@Example.comandada@example.comare treated as two different accounts: the same person canregister twice, and someone who signed up with one casing can be locked out when
they log in with another. Leading/trailing whitespace has the same effect and,
worse,
ada@example.comfails@IsEmail()outright with a 400.What this does: it normalizes the email input (trim, then lowercase) before
validation runs, on register, login, and the
GET /users/lookupquery. I didthis with a
class-transformer@Transformon theemailfield of each DTO.ValidationPipeis already configured withtransform: true, so the servicelayer and the unique constraint now always see one canonical form. This matches
the DTO style already in the repo, which uses
class-transformer(@Typeincreate-event.dto.ts).I included the users lookup query on purpose. The issue title says "register and
login," but
LookupQueryinusers.controller.tshad the identical defect:@IsEmail()with no normalization, feedingusersService.lookupByEmail(...), soGET /users/lookup?email=User@x.comwould not resolve an account stored asuser@x.com. It is the same one logical change, so leaving it out would haveshipped half a fix. I moved that inline class into
users/dto/lookup-query.dto.tsso it lives next to the other DTOs and can be unit-tested the same way.
The transform keeps a
typeof value === 'string'guard and returns the valueuntouched otherwise, so a missing or non-string email still falls through to
@IsEmail()and is rejected exactly as before, rather than throwing onundefined.toLowerCase(). There is a test that locks that behavior in.Tests: I added assertions to the existing
register.dto.spec.ts,login.dto.spec.ts, and a newlookup-query.dto.spec.tsthat feed a mixed-case,whitespace-padded address and assert the normalized result, plus the
non-string passthrough case. Each normalization test fails if the transform is
removed, so they actually pin the behavior.
On the migration the issue mentions: I deliberately did not add a data backfill.
This repo is pre-release (0.0.1, a single init migration, no meaningful
production rows), and a blind
UPDATE users SET email = lower(email)can collideon the unique constraint if two case-variant rows already exist. Resolving that
needs a dedupe/merge decision that is really the maintainer's call, so it does
not belong in a good-first-issue. The consequence worth naming in the same
breath: once login normalizes input, any pre-existing mixed-case row would not be
able to authenticate until such a backfill runs. That is fine here given there
are effectively no real rows, but I want it stated rather than discovered. Happy
to send the backfill (with the dedupe handling) as a follow-up if you want it.