Make email address lookups case-insensitive
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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));
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user