Make email address lookups case-insensitive

This commit is contained in:
Quentin Gliech
2025-07-08 18:01:20 +02:00
parent a84e1718db
commit 39b3dbe5db
6 changed files with 38 additions and 14 deletions

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n WHERE email = $1\n ",
"query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n WHERE LOWER(email) = LOWER($1)\n ",
"describe": {
"columns": [
{
@@ -36,5 +36,5 @@
false
]
},
"hash": "f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec"
"hash": "5eea2f4c3e82ae606b09b8a81332594c97ba0afe972f0fee145b6094789fb6c7"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n\n WHERE user_id = $1 AND email = $2\n ",
"query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n\n WHERE user_id = $1 AND LOWER(email) = LOWER($2)\n ",
"describe": {
"columns": [
{
@@ -37,5 +37,5 @@
false
]
},
"hash": "f7d26de1d380e3e52f47f2b89ed7506e1e4cca72682bc7737e6508dc4015b8d5"
"hash": "ca093cab5143bb3dded2eda9e82473215f4d3c549ea2c5a4f860a102cc46a667"
}

View File

@@ -0,0 +1,11 @@
-- no-transaction
-- Copyright 2025 New Vector Ltd.
--
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
-- Please see LICENSE in the repository root for full details.
-- When we're looking up an email address, we want to be able to do a case-insensitive
-- lookup, so we index the email address lowercase and request it like that
CREATE INDEX CONCURRENTLY
user_emails_lower_email_idx
ON user_emails (LOWER(email));

View File

@@ -15,7 +15,7 @@ use mas_storage::{
user::{UserEmailFilter, UserEmailRepository},
};
use rand::RngCore;
use sea_query::{Expr, PostgresQueryBuilder, Query, enum_def};
use sea_query::{Expr, Func, PostgresQueryBuilder, Query, SimpleExpr, enum_def};
use sea_query_binder::SqlxBinder;
use sqlx::PgConnection;
use ulid::Ulid;
@@ -110,10 +110,13 @@ impl Filter for UserEmailFilter<'_> {
.add_option(self.user().map(|user| {
Expr::col((UserEmails::Table, UserEmails::UserId)).eq(Uuid::from(user.id))
}))
.add_option(
self.email()
.map(|email| Expr::col((UserEmails::Table, UserEmails::Email)).eq(email)),
)
.add_option(self.email().map(|email| {
SimpleExpr::from(Func::lower(Expr::col((
UserEmails::Table,
UserEmails::Email,
))))
.eq(Func::lower(email))
}))
}
}
@@ -175,7 +178,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
, created_at
FROM user_emails
WHERE user_id = $1 AND email = $2
WHERE user_id = $1 AND LOWER(email) = LOWER($2)
"#,
Uuid::from(user.id),
email,
@@ -209,7 +212,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
, email
, created_at
FROM user_emails
WHERE email = $1
WHERE LOWER(email) = LOWER($1)
"#,
email,
)

View File

@@ -268,6 +268,10 @@ async fn test_user_repo_find_by_username(pool: PgPool) {
async fn test_user_email_repo(pool: PgPool) {
const USERNAME: &str = "john";
const EMAIL: &str = "john@example.com";
// This is what is stored in the database, making sure that:
// 1. we don't normalize the email address when storing it
// 2. looking it up is case-incensitive
const UPPERCASE_EMAIL: &str = "JOHN@EXAMPLE.COM";
let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
let mut rng = ChaChaRng::seed_from_u64(42);
@@ -295,12 +299,12 @@ async fn test_user_email_repo(pool: PgPool) {
let user_email = repo
.user_email()
.add(&mut rng, &clock, &user, EMAIL.to_owned())
.add(&mut rng, &clock, &user, UPPERCASE_EMAIL.to_owned())
.await
.unwrap();
assert_eq!(user_email.user_id, user.id);
assert_eq!(user_email.email, EMAIL);
assert_eq!(user_email.email, UPPERCASE_EMAIL);
// Check the counts
assert_eq!(repo.user_email().count(all).await.unwrap(), 1);
@@ -321,7 +325,7 @@ async fn test_user_email_repo(pool: PgPool) {
.expect("user email was not found");
assert_eq!(user_email.user_id, user.id);
assert_eq!(user_email.email, EMAIL);
assert_eq!(user_email.email, UPPERCASE_EMAIL);
// Listing the user emails should work
let emails = repo

View File

@@ -36,6 +36,8 @@ impl<'a> UserEmailFilter<'a> {
}
/// Filter for emails matching a specific email address
///
/// The email address is case-insensitive
#[must_use]
pub fn for_email(mut self, email: &'a str) -> Self {
self.email = Some(email);
@@ -81,6 +83,8 @@ pub trait UserEmailRepository: Send + Sync {
/// Lookup an [`UserEmail`] by its email address for a [`User`]
///
/// The email address is case-insensitive
///
/// Returns `None` if no matching [`UserEmail`] was found
///
/// # Parameters
@@ -95,6 +99,8 @@ pub trait UserEmailRepository: Send + Sync {
/// Lookup an [`UserEmail`] by its email address
///
/// The email address is case-insensitive
///
/// Returns `None` if no matching [`UserEmail`] was found or if multiple
/// [`UserEmail`] are found
///