Portal Username Matching — Case-Insensitive (2026-07-31)
Problem: Portal usernames are email addresses, but every portal_users
lookup was exact case-sensitive match. Phone keyboards autocapitalize the
first letter of the login field, so “Julia.marketis@jeswork.com” + correct
password returned 401. Bit Julia Marketis (JES Baltimore scheduler) on
2026-07-31.
Fix (entirely-api 03e7f9c): services/portal_auth.py gains
_normalize_username() (strip + lowercase), applied at the top of every
service function that takes a username: authenticate, get_user,
create_user, update_user, set_active, reset_password,
change_password, delete_user, touch_session. Routes needed no changes —
all portal_users writes go through the service, and JWT sub carries the
DB-stored (lowercase) username.
Why no COLLATE NOCASE / migration: live DB verified zero rows where
username != trim(lower(username)), so stored data was already normalized.
create_user now stores lowercase, so the case-sensitive UNIQUE constraint
cannot admit case-variant duplicates going forward.
Tests: tests/test_portal_auth_username_case.py (10 tests — autocap
variant, whitespace, case-variant duplicate rejection, wrong-password still
401). Suite 313 passed host-side (test_cert_pipeline pillow_heif collection
skip pre-existing).
Verified live: API rebuilt, worker/scheduler restarted; login with “SCHEDULER1” and ” Scheduler1 ” → 200 with normalized username in response; wrong password → 401.