Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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));
17 changes: 10 additions & 7 deletions crates/storage-pg/src/user/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
}))
}
}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -209,7 +212,7 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
, email
, created_at
FROM user_emails
WHERE email = $1
WHERE LOWER(email) = LOWER($1)
"#,
email,
)
Expand Down
10 changes: 7 additions & 3 deletions crates/storage-pg/src/user/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]";
// 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 = "[email protected]";

let mut repo = PgRepository::from_pool(&pool).await.unwrap().boxed();
let mut rng = ChaChaRng::seed_from_u64(42);
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions crates/storage/src/user/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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
///
Expand Down
Loading