feat(labrinth): ignore email case differences in password recovery flow (#3771)
* feat(labrinth): ignore email case differences in password recovery flow * chore(labrinth): run `sqlx prepare`
This commit is contained in:
parent
a2e323c9ee
commit
ee8ee7af82
22
apps/labrinth/.sqlx/query-889a4f79b7031436b3ed31d1005dc9b378ca9c97a128366cae97649503d5dfdf.json
generated
Normal file
22
apps/labrinth/.sqlx/query-889a4f79b7031436b3ed31d1005dc9b378ca9c97a128366cae97649503d5dfdf.json
generated
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n SELECT id FROM users\n WHERE LOWER(email) = LOWER($1)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Int8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Text"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "889a4f79b7031436b3ed31d1005dc9b378ca9c97a128366cae97649503d5dfdf"
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX users_lowercase_email ON users (LOWER(email));
|
@ -224,24 +224,46 @@ impl DBUser {
|
|||||||
Ok(val)
|
Ok(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_email<'a, E>(
|
pub async fn get_by_email<'a, E>(
|
||||||
email: &str,
|
email: &str,
|
||||||
exec: E,
|
exec: E,
|
||||||
) -> Result<Option<DBUserId>, sqlx::Error>
|
) -> Result<Option<DBUserId>, sqlx::Error>
|
||||||
where
|
where
|
||||||
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
{
|
{
|
||||||
let user_pass = sqlx::query!(
|
let user = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT id FROM users
|
SELECT id FROM users
|
||||||
WHERE email = $1
|
WHERE email = $1
|
||||||
",
|
",
|
||||||
email
|
email
|
||||||
)
|
)
|
||||||
|
.map(|row| DBUserId(row.id))
|
||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user_pass.map(|x| DBUserId(x.id)))
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_case_insensitive_email<'a, E>(
|
||||||
|
email: &str,
|
||||||
|
exec: E,
|
||||||
|
) -> Result<Vec<DBUserId>, sqlx::Error>
|
||||||
|
where
|
||||||
|
E: sqlx::Executor<'a, Database = sqlx::Postgres> + Copy,
|
||||||
|
{
|
||||||
|
let users = sqlx::query!(
|
||||||
|
"
|
||||||
|
SELECT id FROM users
|
||||||
|
WHERE LOWER(email) = LOWER($1)
|
||||||
|
",
|
||||||
|
email
|
||||||
|
)
|
||||||
|
.map(|row| DBUserId(row.id))
|
||||||
|
.fetch_all(exec)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(users)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_projects<'a, E>(
|
pub async fn get_projects<'a, E>(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::auth::email::send_email;
|
use crate::auth::email::send_email;
|
||||||
use crate::auth::validate::get_user_record_from_bearer_token;
|
use crate::auth::validate::get_user_record_from_bearer_token;
|
||||||
use crate::auth::{AuthProvider, AuthenticationError, get_user_from_headers};
|
use crate::auth::{AuthProvider, AuthenticationError, get_user_from_headers};
|
||||||
|
use crate::database::models::DBUser;
|
||||||
use crate::database::models::flow_item::DBFlow;
|
use crate::database::models::flow_item::DBFlow;
|
||||||
use crate::database::redis::RedisPool;
|
use crate::database::redis::RedisPool;
|
||||||
use crate::file_hosting::FileHost;
|
use crate::file_hosting::FileHost;
|
||||||
@ -76,7 +77,7 @@ impl TempUser {
|
|||||||
redis: &RedisPool,
|
redis: &RedisPool,
|
||||||
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
) -> Result<crate::database::models::DBUserId, AuthenticationError> {
|
||||||
if let Some(email) = &self.email {
|
if let Some(email) = &self.email {
|
||||||
if crate::database::models::DBUser::get_email(email, client)
|
if crate::database::models::DBUser::get_by_email(email, client)
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
@ -1385,7 +1386,10 @@ pub async fn create_account_with_password(
|
|||||||
.hash_password(new_account.password.as_bytes(), &salt)?
|
.hash_password(new_account.password.as_bytes(), &salt)?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if crate::database::models::DBUser::get_email(&new_account.email, &**pool)
|
if crate::database::models::DBUser::get_by_email(
|
||||||
|
&new_account.email,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
@ -1450,7 +1454,8 @@ pub async fn create_account_with_password(
|
|||||||
|
|
||||||
#[derive(Deserialize, Validate)]
|
#[derive(Deserialize, Validate)]
|
||||||
pub struct Login {
|
pub struct Login {
|
||||||
pub username: String,
|
#[serde(rename = "username")]
|
||||||
|
pub username_or_email: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
pub challenge: String,
|
pub challenge: String,
|
||||||
}
|
}
|
||||||
@ -1466,14 +1471,17 @@ pub async fn login_password(
|
|||||||
return Err(ApiError::Turnstile);
|
return Err(ApiError::Turnstile);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = if let Some(user) =
|
let user = if let Some(user) = crate::database::models::DBUser::get(
|
||||||
crate::database::models::DBUser::get(&login.username, &**pool, &redis)
|
&login.username_or_email,
|
||||||
|
&**pool,
|
||||||
|
&redis,
|
||||||
|
)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
user
|
user
|
||||||
} else {
|
} else {
|
||||||
let user = crate::database::models::DBUser::get_email(
|
let user = crate::database::models::DBUser::get_by_email(
|
||||||
&login.username,
|
&login.username_or_email,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@ -1903,7 +1911,8 @@ pub async fn remove_2fa(
|
|||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ResetPassword {
|
pub struct ResetPassword {
|
||||||
pub username: String,
|
#[serde(rename = "username")]
|
||||||
|
pub username_or_email: String,
|
||||||
pub challenge: String,
|
pub challenge: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1918,30 +1927,62 @@ pub async fn reset_password_begin(
|
|||||||
return Err(ApiError::Turnstile);
|
return Err(ApiError::Turnstile);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = if let Some(user_id) =
|
let user =
|
||||||
crate::database::models::DBUser::get_email(
|
match crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||||
&reset_password.username,
|
&reset_password.username_or_email,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?[..]
|
||||||
{
|
{
|
||||||
crate::database::models::DBUser::get_id(user_id, &**pool, &redis)
|
[] => {
|
||||||
.await?
|
// Try finding by username or ID
|
||||||
} else {
|
|
||||||
crate::database::models::DBUser::get(
|
crate::database::models::DBUser::get(
|
||||||
&reset_password.username,
|
&reset_password.username_or_email,
|
||||||
&**pool,
|
&**pool,
|
||||||
&redis,
|
&redis,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
}
|
||||||
|
[user_id] => {
|
||||||
|
// If there is only one user with the given email, ignoring case,
|
||||||
|
// we can assume it's the user we want to reset the password for
|
||||||
|
crate::database::models::DBUser::get_id(
|
||||||
|
user_id, &**pool, &redis,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// When several users use variations of the same email with
|
||||||
|
// different cases, we cannot reliably tell which user should
|
||||||
|
// receive the password reset email, so fall back to case sensitive
|
||||||
|
// search to avoid spamming multiple users
|
||||||
|
if let Some(user_id) =
|
||||||
|
crate::database::models::DBUser::get_by_email(
|
||||||
|
&reset_password.username_or_email,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
crate::database::models::DBUser::get_id(
|
||||||
|
user_id, &**pool, &redis,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(user) = user {
|
if let Some(DBUser {
|
||||||
let flow = DBFlow::ForgotPassword { user_id: user.id }
|
id: user_id,
|
||||||
|
email: Some(email),
|
||||||
|
..
|
||||||
|
}) = user
|
||||||
|
{
|
||||||
|
let flow = DBFlow::ForgotPassword { user_id }
|
||||||
.insert(Duration::hours(24), &redis)
|
.insert(Duration::hours(24), &redis)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(email) = user.email {
|
|
||||||
send_email(
|
send_email(
|
||||||
email,
|
email,
|
||||||
"Reset your password",
|
"Reset your password",
|
||||||
@ -1958,7 +1999,6 @@ pub async fn reset_password_begin(
|
|||||||
)),
|
)),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(HttpResponse::Ok().finish())
|
Ok(HttpResponse::Ok().finish())
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user