2.9 KiB
2.9 KiB
Postmortem: Duplicate User Accounts from Case-Sensitive Email Handling
Date: 2026-01-29 Severity: Medium Status: Resolved
Summary
Users reported receiving duplicate email notifications. Investigation revealed multiple user accounts existed with the same email address differing only by case (e.g., user@example.com and User@Example.com).
Impact
- 6 users affected with duplicate accounts
- Duplicate email notifications sent to affected users
- User data split across multiple accounts
Root Cause
The email field in the User model used case-sensitive matching for both:
- Uniqueness constraint validation
- User lookup during authentication
This allowed users to create multiple accounts by varying the case of their email address during signup.
Timeline
- 2026-01-29 08:00 - User report of duplicate notifications received
- 2026-01-29 08:15 - Investigation began via production database
- 2026-01-29 08:30 - Root cause identified: case-sensitive email handling
- 2026-01-29 08:45 - Fix developed and tested locally
- 2026-01-29 09:00 - Fix deployed to production
- 2026-01-29 09:15 - Merge script created to consolidate duplicate accounts
- 2026-01-29 09:30 - All duplicate accounts merged, issue resolved
Resolution
Code Changes
-
Email normalization on creation (
remarkbox/models/user.py)- Emails now stored as lowercase in
User.__init__
- Emails now stored as lowercase in
-
Case-insensitive lookup (
remarkbox/models/user.py)get_user_by_email()now usesfunc.lower()for comparison
-
Merge script (
remarkbox/scripts/merge_duplicate_email_users.py)- New management command to find and merge duplicate accounts
- Keeps oldest account, transfers all related data
- Handles unique constraint conflicts gracefully
Data Migration
Ran remarkbox_merge_duplicate_email_users to consolidate:
- 6 duplicate email sets merged
- 14 nodes transferred
- 4 watchers transferred
- 1 notification transferred
- 6 duplicate accounts deleted
- All emails normalized to lowercase
Prevention
-
Unit tests added (
test_models.py)test_email_normalized_to_lowercasetest_email_mixed_case_normalized
-
Integration tests added (
test_views.py)test_get_user_by_email_case_insensitivetest_get_or_create_returns_existing_regardless_of_casetest_new_user_email_stored_lowercasetest_no_duplicate_accounts_from_case_variations
Lessons Learned
- Email addresses should always be normalized to lowercase on input
- Case-insensitive comparison should be used for email lookups
- Database unique constraints alone don't prevent case-variant duplicates in SQLite
Action Items
- Fix email normalization in User model
- Fix case-insensitive email lookup
- Create merge script for existing duplicates
- Add regression tests
- Run merge on production
- Document in postmortem