-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Case-insensitive username and email fields
Problem ------- It's common for typos to occur when entering usernames and emails that result in random incorrect capitalization. Additionally, it's both a security vulnerability and a usability issue if multiple users can have the same username/email just with different capitalization. Solution -------- - Add a case-insensitive collation (we only officially support this on Postgres - Assign the Username and Email columns of the User table to use the case-insensitive collation.
- Loading branch information
1 parent
81f6b13
commit 383c4c3
Showing
21 changed files
with
485 additions
and
28 deletions.
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
src/migration/collation/m20241022_065427_case_insensitive_collation.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
//! Migration to create a case-insensitive collation. | ||
//! | ||
//! See: <https://www.postgresql.org/docs/current/collation.html#COLLATION-NONDETERMINISTIC> | ||
//! | ||
//! Note: Currently only supports Postgres. If another DB is used, will do nothing. | ||
use crate::migration::collation::{ | ||
exec_create_case_insensitive_collation, exec_drop_case_insensitive_collation, | ||
}; | ||
use async_trait::async_trait; | ||
use sea_orm_migration::prelude::*; | ||
|
||
#[derive(DeriveMigrationName)] | ||
pub struct Migration; | ||
|
||
#[async_trait] | ||
impl MigrationTrait for Migration { | ||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
exec_create_case_insensitive_collation(manager).await | ||
} | ||
|
||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
exec_drop_case_insensitive_collation(manager).await | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
//! Utilities and migrations related to collations. | ||
pub mod m20241022_065427_case_insensitive_collation; | ||
|
||
use sea_orm::{DbBackend, Statement}; | ||
use sea_orm_migration::prelude::*; | ||
|
||
/// Collations available from Roadster. | ||
#[derive(DeriveIden)] | ||
#[non_exhaustive] | ||
pub enum Collation { | ||
/// The `default` collation. This comes included in Postgres. | ||
/// | ||
/// Note: This iden needs to be surrounded in quotes, at least in Postgres. | ||
Default, | ||
/// A case-insensitive collation. | ||
CaseInsensitive, | ||
} | ||
|
||
/// Wrapper around [`create_case_insensitive_collation`] to execute the returned [`Statement`], if | ||
/// present. | ||
/// | ||
/// # Examples | ||
/// ```rust | ||
/// use roadster::migration::collation::exec_create_case_insensitive_collation; | ||
/// use sea_orm_migration::prelude::*; | ||
/// | ||
/// #[derive(DeriveMigrationName)] | ||
/// pub struct Migration; | ||
/// | ||
/// #[async_trait::async_trait] | ||
/// impl MigrationTrait for Migration { | ||
/// async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
/// exec_create_case_insensitive_collation(manager).await | ||
/// } | ||
/// # | ||
/// # async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
/// # todo!() | ||
/// # } | ||
/// } | ||
/// ``` | ||
pub async fn exec_create_case_insensitive_collation( | ||
manager: &SchemaManager<'_>, | ||
) -> Result<(), DbErr> { | ||
let statement = create_case_insensitive_collation(manager); | ||
if let Some(statement) = statement { | ||
manager.get_connection().execute(statement).await?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Create a case-insensitive collation. | ||
/// | ||
/// See: <https://www.postgresql.org/docs/current/collation.html#COLLATION-NONDETERMINISTIC> | ||
/// | ||
/// Note: Currently only supports Postgres. If another DB is used, will return [`None`]. | ||
pub fn create_case_insensitive_collation(manager: &SchemaManager<'_>) -> Option<Statement> { | ||
let backend = manager.get_database_backend(); | ||
create_case_insensitive_collation_for_db_backend(backend) | ||
} | ||
|
||
fn create_case_insensitive_collation_for_db_backend(backend: DbBackend) -> Option<Statement> { | ||
if let DbBackend::Postgres = backend { | ||
Some(Statement::from_string( | ||
backend, | ||
format!( | ||
r#"CREATE COLLATION IF NOT EXISTS {} ( | ||
provider = icu, | ||
locale = 'und-u-ks-level2', | ||
deterministic = false | ||
); | ||
"#, | ||
Collation::CaseInsensitive.to_string() | ||
), | ||
)) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
/// Wrapper around [`drop_case_insensitive_collation`] to execute the returned [`Statement`], if | ||
/// present. | ||
/// | ||
/// # Examples | ||
/// ```rust | ||
/// use roadster::migration::collation::exec_drop_case_insensitive_collation; | ||
/// use sea_orm_migration::prelude::*; | ||
/// | ||
/// #[derive(DeriveMigrationName)] | ||
/// pub struct Migration; | ||
/// | ||
/// #[async_trait::async_trait] | ||
/// impl MigrationTrait for Migration { | ||
/// # async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
/// # todo!() | ||
/// # } | ||
/// # | ||
/// async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { | ||
/// exec_drop_case_insensitive_collation(manager).await | ||
/// } | ||
/// } | ||
/// ``` | ||
pub async fn exec_drop_case_insensitive_collation( | ||
manager: &SchemaManager<'_>, | ||
) -> Result<(), DbErr> { | ||
let statement = drop_case_insensitive_collation(manager); | ||
if let Some(statement) = statement { | ||
manager.get_connection().execute(statement).await?; | ||
} | ||
|
||
Ok(()) | ||
} | ||
|
||
/// Drop the case-insensitive collation that was previously created by [`create_case_insensitive_collation`]. | ||
/// | ||
/// Note: Currently only supports Postgres. If another DB is used, will return [None]. | ||
pub fn drop_case_insensitive_collation(manager: &SchemaManager<'_>) -> Option<Statement> { | ||
let backend = manager.get_database_backend(); | ||
drop_case_insensitive_collation_for_db_backend(backend) | ||
} | ||
|
||
fn drop_case_insensitive_collation_for_db_backend(backend: DbBackend) -> Option<Statement> { | ||
if let DbBackend::Postgres = backend { | ||
Some(Statement::from_string( | ||
backend, | ||
format!( | ||
"DROP COLLATION IF EXISTS {};", | ||
Collation::CaseInsensitive.to_string() | ||
), | ||
)) | ||
} else { | ||
None | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use crate::testing::snapshot::TestCase; | ||
use insta::assert_debug_snapshot; | ||
use rstest::{fixture, rstest}; | ||
use sea_orm::DbBackend; | ||
|
||
#[fixture] | ||
fn case() -> TestCase { | ||
Default::default() | ||
} | ||
|
||
#[rstest] | ||
#[case(DbBackend::Postgres)] | ||
#[case(DbBackend::MySql)] | ||
#[case(DbBackend::Sqlite)] | ||
#[cfg_attr(coverage_nightly, coverage(off))] | ||
fn create_case_insensitive_collation_for_db_backend( | ||
_case: TestCase, | ||
#[case] backend: DbBackend, | ||
) { | ||
let statement = super::create_case_insensitive_collation_for_db_backend(backend); | ||
|
||
assert_debug_snapshot!(statement); | ||
} | ||
|
||
#[rstest] | ||
#[case(DbBackend::Postgres)] | ||
#[case(DbBackend::MySql)] | ||
#[case(DbBackend::Sqlite)] | ||
#[cfg_attr(coverage_nightly, coverage(off))] | ||
fn drop_case_insensitive_collation_for_db_backend(_case: TestCase, #[case] backend: DbBackend) { | ||
let statement = super::drop_case_insensitive_collation_for_db_backend(backend); | ||
|
||
assert_debug_snapshot!(statement); | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
...migration__collation__tests__create_case_insensitive_collation_for_db_backend@case_1.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
Some( | ||
Statement { | ||
sql: "CREATE COLLATION IF NOT EXISTS case_insensitive (\nprovider = icu,\nlocale = 'und-u-ks-level2',\ndeterministic = false\n);\n", | ||
values: None, | ||
db_backend: Postgres, | ||
}, | ||
) |
5 changes: 5 additions & 0 deletions
5
...migration__collation__tests__create_case_insensitive_collation_for_db_backend@case_2.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
None |
5 changes: 5 additions & 0 deletions
5
...migration__collation__tests__create_case_insensitive_collation_for_db_backend@case_3.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
None |
11 changes: 11 additions & 0 deletions
11
...__migration__collation__tests__drop_case_insensitive_collation_for_db_backend@case_1.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
Some( | ||
Statement { | ||
sql: "DROP COLLATION IF EXISTS case_insensitive;", | ||
values: None, | ||
db_backend: Postgres, | ||
}, | ||
) |
5 changes: 5 additions & 0 deletions
5
...__migration__collation__tests__drop_case_insensitive_collation_for_db_backend@case_2.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
None |
5 changes: 5 additions & 0 deletions
5
...__migration__collation__tests__drop_case_insensitive_collation_for_db_backend@case_3.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
source: src/migration/collation/mod.rs | ||
expression: statement | ||
--- | ||
None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
src/migration/timestamp/m20240723_201404_add_update_timestamp_function.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.