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

@@ -28,9 +28,17 @@ pub async fn create(pool: &PgPool, username: &str, password_hash: &str) -> AppRe
}
}
/// Case-insensitive lookup so login with "Alice" matches a user
/// registered as "alice" (the unique index on `lower(username)` keeps
/// the comparison cheap). Equivalent in spirit to ILIKE but uses the
/// functional index directly.
pub async fn find_by_username(pool: &PgPool, username: &str) -> AppResult<Option<User>> {
let row = sqlx::query_as::<_, User>(
r#"SELECT id, username, password_hash, created_at FROM users WHERE username = $1"#,
r#"
SELECT id, username, password_hash, created_at
FROM users
WHERE lower(username) = lower($1)
"#,
)
.bind(username)
.fetch_optional(pool)