bugfix: case-insensitive usernames, reject non-positive bookmark page

Two related correctness fixes from the audit:

- Username uniqueness was case-sensitive (`username text UNIQUE`), so
  "Alice" and "alice" could both register and then race on login.
  Migration 0006 adds a unique index on `lower(username)`; the
  existing constraint is kept (overlapping but cheap) to avoid a
  destructive migration on any deployments that may already exist.
  `repo::user::find_by_username` now matches on `lower(username) =
  lower($1)` so login is case-insensitive against the same index.
  Test: registering "alice" then "Alice" returns 409 conflict; login
  with "ALICE" succeeds against the existing user.

- `POST /api/v1/bookmarks` silently accepted `page: 0` and `page: -1`
  even though both are nonsense for a 1-indexed page number. Reject
  with 422 `validation_failed` and `details.page` populated, matching
  the pattern used for missing-metadata / empty-title elsewhere. Test
  covers both 0 and -1.

Lockstep version bump to 0.9.4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-16 23:27:19 +02:00
parent 80ab119750
commit 785b9755cf
8 changed files with 100 additions and 4 deletions

View File

@@ -0,0 +1,15 @@
-- Make username uniqueness case-insensitive.
--
-- The 0001 schema declared `username text NOT NULL UNIQUE`, which let
-- "Alice" and "alice" both register and then race for who gets which
-- session cookie on subsequent logins. Adding a partial / functional
-- unique index over `lower(username)` blocks the conflict at the DB
-- layer regardless of how the application normalises the input.
--
-- The original `username UNIQUE` constraint is kept — it now overlaps
-- with the new index but the duplication is cheap and removing the
-- inline constraint would require a multi-step destructive migration
-- on existing data.
CREATE UNIQUE INDEX users_username_lower_uniq
ON users (lower(username));