Skip to content

fix(user): case-insensitive mail lookup to prevent duplicate accounts#3855

Draft
Blume1977 wants to merge 1 commit into
developfrom
fix/user-case-insensitive-mail-dedup
Draft

fix(user): case-insensitive mail lookup to prevent duplicate accounts#3855
Blume1977 wants to merge 1 commit into
developfrom
fix/user-case-insensitive-mail-dedup

Conversation

@Blume1977

Copy link
Copy Markdown
Collaborator

Problem

A user ended up with two userData accounts for the same e-mail differing only in case — e.g. Samuel.kullmann@startmail.com (328304, created 2025-07-29, Active) and samuel.kullmann@startmail.com (396215, created 2026-05-23, KycOnly).

Root cause (regression from the PSQL migration #3620):

  1. Input lowercase-normalization (Util.toLowerCaseTrim) only arrived with Fix case-sensitive email comparison issues #2695 (2025-12-23) → accounts created earlier hold mixed-case mails.
  2. The single dedup/merge lookup getUsersByMail compared mail by exact equality.
  3. On MSSQL the default collation was case-insensitive, so this matched regardless of case. After the PostgreSQL cutover (PSQL migration #3620, 2026-05-22) the comparison is byte-exact → the lowercased lookup no longer finds the stored mixed-case row, so a second account is created instead of being reused/merged. (Duplicate 396215 appeared one day after the cutover.)

Changes (PR 1 of 2)

  • getUsersByMail now compares LOWER(mail) = :mail with a lowercased parameter (Raw operator, matching the existing repo style in user.repository.ts). Fixes all 5 callers centrally (mail-login reuse, checkMail conflict/merge, recommendation ×2, compliance search).
  • Migration 1781015951873-NormalizeUserDataMailLowercase:
    • lowercases legacy user_data.mail and recommendation.recommendedMail,
    • adds a non-unique functional index IDX_user_data_mail_lower on LOWER(mail).
  • Specs: case-insensitive lookup builds the expected LOWER(...)= :mail with lowercased param; checkMail raises ConflictException on a case-variant duplicate.

Deliberately deferred to PR 2 (gated)

The UNIQUE functional index is a separate migration, to be merged only after the existing duplicate accounts are resolved (the CREATE UNIQUE INDEX would otherwise fail on current data). Its predicate must mirror the dedup set and exclude merged/blocked rows (which legitimately retain their mail):

CREATE UNIQUE INDEX "IDX_user_data_mail_lower" ON "user_data" (LOWER("mail"))
WHERE "mail" IS NOT NULL AND "status" IN ('Active','NA','KycOnly','Deactivated');

Operational follow-up (Ops/Compliance, not code)

Audit + merge existing duplicates:

SELECT LOWER(mail) AS mail_lc, array_agg(id ORDER BY id) AS ids, count(*) AS n
FROM user_data WHERE mail IS NOT NULL
GROUP BY LOWER(mail) HAVING count(*) > 1 ORDER BY n DESC;

Includes the reported case (master 328304 / slave 396215). Unmergeable pairs (AML / verified-name mismatch / blocked) need a manual compliance decision.

Verification

  • jest user-data.service spec (4) + account-merge specs (12) green.
  • ESLint clean on changed files; migration validated with node --check.

After the MSSQL->PostgreSQL cutover (#3620), the duplicate-detection
lookup `getUsersByMail` used an exact, case-sensitive `mail = ?`
comparison. Accounts created before the input lowercase-normalization
(#2695) hold mixed-case mails, so the lookup no longer matched them and
a second account could be created for the same address
(e.g. `Samuel.kullmann@...` vs `samuel.kullmann@...`).

- getUsersByMail now compares LOWER(mail) = :mail (input lowercased)
- migration lowercases legacy user_data.mail and recommendation.recommendedMail
- migration adds a non-unique functional index on LOWER(mail)
- specs cover the case-insensitive lookup and the conflict path

The UNIQUE variant of the index is intentionally deferred to a separate,
gated migration once the existing duplicate accounts are merged.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

⚠️ Unverified Commits (1)

The following commits are not signed/verified:

  • aa6c670 fix(user): case-insensitive mail lookup to prevent duplicate accounts (Blume1977)
How to sign commits
# SSH signing (recommended)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true

# Re-sign last commit
git commit --amend -S --no-edit
git push --force-with-lease

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