diff --git a/geneit_backend/Cargo.lock b/geneit_backend/Cargo.lock index 562b827..a28ecb5 100644 --- a/geneit_backend/Cargo.lock +++ b/geneit_backend/Cargo.lock @@ -320,6 +320,19 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f1e31e207a6b8fb791a38ea3105e6cb541f55e4d029902d3039a4ad07cc4105" +[[package]] +name = "bcrypt" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a" +dependencies = [ + "base64", + "blowfish", + "getrandom", + "subtle", + "zeroize", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -335,6 +348,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "brotli" version = "3.3.4" @@ -392,6 +415,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.3.0" @@ -734,6 +767,7 @@ dependencies = [ "actix-remote-ip", "actix-web", "anyhow", + "bcrypt", "clap", "diesel", "env_logger", @@ -874,6 +908,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1516,6 +1559,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -1894,6 +1943,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" + [[package]] name = "zstd" version = "0.12.3+zstd.1.5.2" diff --git a/geneit_backend/Cargo.toml b/geneit_backend/Cargo.toml index 561c8b3..3bfb90d 100644 --- a/geneit_backend/Cargo.toml +++ b/geneit_backend/Cargo.toml @@ -19,4 +19,5 @@ actix-remote-ip = "0.1.0" mailchecker = "5.0.9" redis = "0.23.0" lettre = "0.10.4" -rand = "0.8.5" \ No newline at end of file +rand = "0.8.5" +bcrypt = "0.14.0" \ No newline at end of file diff --git a/geneit_backend/src/controllers/auth_controller.rs b/geneit_backend/src/controllers/auth_controller.rs index bccd011..d8e5f48 100644 --- a/geneit_backend/src/controllers/auth_controller.rs +++ b/geneit_backend/src/controllers/auth_controller.rs @@ -91,3 +91,50 @@ pub async fn check_reset_password_token( Ok(HttpResponse::Ok().json(CheckResetPasswordTokenResponse { name: user.name })) } + +#[derive(serde::Deserialize)] +pub struct ResetPasswordBody { + token: String, + password: String, +} + +/// Reset password +pub async fn reset_password(remote_ip: RemoteIP, req: web::Json) -> HttpResult { + // Rate limiting + if rate_limiter_service::should_block_action( + remote_ip.0, + RatedAction::CheckResetPasswordTokenFailed, + ) + .await? + { + return Ok(HttpResponse::TooManyRequests().finish()); + } + + let user = match users_service::get_by_pwd_reset_token(&req.token).await { + Ok(t) => t, + Err(e) => { + rate_limiter_service::record_action( + remote_ip.0, + RatedAction::CheckResetPasswordTokenFailed, + ) + .await?; + log::error!("Password reset token could not be used: {}", e); + return Ok(HttpResponse::NotFound().finish()); + } + }; + + if !StaticConstraints::default() + .password_len + .validate(&req.password) + { + return Ok(HttpResponse::BadRequest().json("Taille du mot de passe invalide!")); + } + + // Validate account, if required + users_service::validate_account(&user).await?; + + // Change user password + users_service::change_password(&user, &req.password).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 7961389..fd71cd7 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -31,6 +31,10 @@ async fn main() -> std::io::Result<()> { "/auth/check_reset_password_token", web::post().to(auth_controller::check_reset_password_token), ) + .route( + "/auth/reset_password", + web::post().to(auth_controller::reset_password), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/services/users_service.rs b/geneit_backend/src/services/users_service.rs index caef213..bb1bf2c 100644 --- a/geneit_backend/src/services/users_service.rs +++ b/geneit_backend/src/services/users_service.rs @@ -8,7 +8,9 @@ use crate::schema::users; use crate::services::mail_service; use crate::utils::string_utils::rand_str; use crate::utils::time_utils::time; +use bcrypt::DEFAULT_COST; use diesel::prelude::*; +use std::io::ErrorKind; /// Get the information of the user, by its id pub async fn get_by_id(id: UserID) -> anyhow::Result { @@ -17,6 +19,13 @@ pub async fn get_by_id(id: UserID) -> anyhow::Result { /// Get the information of the user, by its password reset token pub async fn get_by_pwd_reset_token(token: &str) -> anyhow::Result { + if token.is_empty() { + return Err(anyhow::Error::from(std::io::Error::new( + ErrorKind::Other, + "Token is empty!", + ))); + } + db_connection::execute(|conn| { Ok(users::table .filter( @@ -109,3 +118,52 @@ pub async fn delete_not_validated_accounts() -> anyhow::Result<()> { Ok(()) }) } + +/// Mark account as validated +pub async fn validate_account(user: &User) -> anyhow::Result<()> { + if user.time_activate > 0 { + log::debug!( + "Did not activate account {} because it is already activated!", + user.id + ); + return Ok(()); + } + + mail_service::send_mail( + &user.email, + "Activation de votre compte GeneIT", + "Bonjour,\n\n\ + Votre compte GeneIT a été activé avec succès !\n\n\ + Cordialement,\n\n\ + L'équipe de GeneIT", + ) + .await?; + + db_connection::execute(|conn| { + Ok( + diesel::update(users::dsl::users.filter(users::dsl::id.eq(user.id))) + .set((users::dsl::time_activate.eq(time() as i64),)) + .execute(conn)?, + ) + })?; + + Ok(()) +} + +/// Change user paswsord +pub async fn change_password(user: &User, new_password: &str) -> anyhow::Result<()> { + let hash = bcrypt::hash(new_password, DEFAULT_COST)?; + + db_connection::execute(|conn| { + Ok( + diesel::update(users::dsl::users.filter(users::dsl::id.eq(user.id))) + .set(( + users::dsl::password.eq(hash), + users::dsl::reset_password_token.eq(None::), + )) + .execute(conn)?, + ) + })?; + + Ok(()) +}