From 83e6871997dbef837579f659444b7b7b322fc236 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Tue, 5 Apr 2022 17:17:34 +0200 Subject: [PATCH] Can change user password --- src/actors/users_actor.rs | 4 +- src/controllers/settings_controller.rs | 89 ++++++++++++++++-- src/data/mod.rs | 1 + src/data/remote_ip.rs | 28 ++++++ src/data/user.rs | 10 +- src/main.rs | 2 + templates/settings/base_settings_page.html | 104 ++++++++++++--------- templates/settings/change_password.html | 57 +++++++++++ 8 files changed, 236 insertions(+), 59 deletions(-) create mode 100644 src/data/remote_ip.rs create mode 100644 templates/settings/change_password.html diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index b1fe222..3f539f5 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -1,7 +1,7 @@ use actix::{Actor, Context, Handler, Message, MessageResult}; use crate::data::entity_manager::EntityManager; -use crate::data::user::{User, UserID, verify_password}; +use crate::data::user::{User, UserID}; #[derive(Debug)] pub enum LoginResult { @@ -57,7 +57,7 @@ impl Handler for UsersActor { match self.manager.find_by_username_or_email(&msg.login) { None => MessageResult(LoginResult::AccountNotFound), Some(user) => { - if !verify_password(msg.password, &user.password) { + if !user.verify_password(&msg.password) { return MessageResult(LoginResult::InvalidPassword); } diff --git a/src/controllers/settings_controller.rs b/src/controllers/settings_controller.rs index 2e4256d..379e409 100644 --- a/src/controllers/settings_controller.rs +++ b/src/controllers/settings_controller.rs @@ -3,9 +3,11 @@ use actix_identity::Identity; use actix_web::{HttpResponse, Responder, web}; use askama::Template; -use crate::actors::users_actor; +use crate::actors::{bruteforce_actor, users_actor}; +use crate::actors::bruteforce_actor::BruteForceActor; use crate::actors::users_actor::UsersActor; -use crate::constants::APP_NAME; +use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN}; +use crate::data::remote_ip::RemoteIP; use crate::data::session_identity::SessionIdentity; use crate::data::user::User; @@ -21,11 +23,12 @@ struct BaseSettingsPage { } impl BaseSettingsPage { - async fn get(user: &User) -> BaseSettingsPage { + async fn get(page_title: &'static str, user: &User, + danger_message: Option, success_message: Option) -> BaseSettingsPage { Self { - danger_message: None, - success_message: None, - page_title: "Account details", + danger_message, + success_message, + page_title, app_name: APP_NAME, is_admin: user.admin, user_name: user.username.to_string(), @@ -44,19 +47,87 @@ struct AccountDetailsPage { email: String, } +#[derive(Template)] +#[template(path = "settings/change_password.html")] +struct ChangePasswordPage { + _parent: BaseSettingsPage, + min_pwd_len: usize, +} + /// Account details page -pub async fn account_settings_details_route(id: Identity, user_actor: web::Data>) -> impl Responder { - let user: User = user_actor.send( +pub async fn account_settings_details_route(id: Identity, users: web::Data>) -> impl Responder { + let user: User = users.send( users_actor::GetUserRequest(SessionIdentity(&id).user_id()) ).await.unwrap().0.unwrap(); HttpResponse::Ok() .body(AccountDetailsPage { - _parent: BaseSettingsPage::get(&user).await, + _parent: BaseSettingsPage::get("Account details", &user, None, None).await, user_id: user.uid, first_name: user.first_name, last_name: user.last_last, username: user.username, email: user.email, }.render().unwrap()) +} + +#[derive(serde::Deserialize)] +pub struct PassChangeRequest { + pub old_pass: String, + pub new_pass: String, +} + +/// Change password route +pub async fn change_password_route(id: Identity, + users: web::Data>, + req: Option>, + bruteforce: web::Data>, + remote_ip: RemoteIP) -> impl Responder { + let mut danger = None; + let mut success = None; + + let user: User = users.send( + users_actor::GetUserRequest(SessionIdentity(&id).user_id()) + ).await.unwrap().0.unwrap(); + + let failed_attempts = bruteforce.send(bruteforce_actor::CountFailedAttempt { ip: remote_ip.into() }).await.unwrap(); + if failed_attempts > MAX_FAILED_LOGIN_ATTEMPTS { + danger = Some("Too many invalid password attempts. Please try to change your password later.".to_string()); + } else if let Some(req) = req { + + // Invalid password + if !user.verify_password(&req.old_pass) { + danger = Some("Old password is invalid!".to_string()); + bruteforce.send(bruteforce_actor::RecordFailedAttempt { ip: remote_ip.into() }).await.unwrap(); + } + + // Password too short + else if req.new_pass.len() < MIN_PASS_LEN { + danger = Some("New password is too short!".to_string()); + } + + // Change password + else { + let res = users.send( + users_actor::ChangePasswordRequest { + user_id: user.uid.clone(), + new_password: req.new_pass.to_string(), + temporary: false, + } + ).await.unwrap().0; + + if !res { + danger = Some("An error occurred while trying to change your password!".to_string()); + } else { + success = Some("Your password was successfully changed!".to_string()); + } + } + } + + + HttpResponse::Ok() + .body(ChangePasswordPage { + _parent: BaseSettingsPage::get("Change password", &user, danger, success).await, + min_pwd_len: MIN_PASS_LEN, + }.render().unwrap()) } \ No newline at end of file diff --git a/src/data/mod.rs b/src/data/mod.rs index bf6248c..29d021e 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -3,3 +3,4 @@ pub mod entity_manager; pub mod service; pub mod session_identity; pub mod user; +pub mod remote_ip; \ No newline at end of file diff --git a/src/data/remote_ip.rs b/src/data/remote_ip.rs new file mode 100644 index 0000000..ccd0669 --- /dev/null +++ b/src/data/remote_ip.rs @@ -0,0 +1,28 @@ +use std::net::IpAddr; + +use actix_web::{Error, FromRequest, HttpRequest, web}; +use actix_web::dev::Payload; +use futures_util::future::{Ready, ready}; + +use crate::data::app_config::AppConfig; +use crate::utils::network_utils::get_remote_ip; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct RemoteIP(pub IpAddr); + +impl Into for RemoteIP { + fn into(self) -> IpAddr { + self.0 + } +} + +impl FromRequest for RemoteIP { + type Error = Error; + type Future = Ready>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + let config: &web::Data = req.app_data().expect("AppData undefined!"); + ready(Ok(RemoteIP(get_remote_ip(req, config.proxy_ip.as_deref())))) + } +} \ No newline at end of file diff --git a/src/data/user.rs b/src/data/user.rs index 9bf53b8..18405dc 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -21,6 +21,12 @@ pub struct User { pub authorized_services: Option>, } +impl User { + pub fn verify_password>(&self, pass: P) -> bool { + verify_password(pass, &self.password) + } +} + impl PartialEq for User { fn eq(&self, other: &Self) -> bool { self.uid.eq(&other.uid) @@ -81,8 +87,8 @@ impl EntityManager { /// Update user information fn update_user(&mut self, id: &UserID, update: F) -> bool - where - F: FnOnce(User) -> User, + where + F: FnOnce(User) -> User, { let user = match self.find_by_user_id(id) { None => return false, diff --git a/src/main.rs b/src/main.rs index cd10222..18b78a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,6 +106,8 @@ async fn main() -> std::io::Result<()> { // Settings routes .route("/settings", web::get().to(settings_controller::account_settings_details_route)) + .route("/settings/change_password", web::get().to(settings_controller::change_password_route)) + .route("/settings/change_password", web::post().to(settings_controller::change_password_route)) }) .bind(listen_address)? .run() diff --git a/templates/settings/base_settings_page.html b/templates/settings/base_settings_page.html index d7556a7..d05dc01 100644 --- a/templates/settings/base_settings_page.html +++ b/templates/settings/base_settings_page.html @@ -1,69 +1,81 @@ - - {{ page_title }} - {{ app_name }} - - - - - + + {{ page_title }} - {{ app_name }} + + + + + -
+ - -
- {% if let Some(msg) = danger_message %}
{{ msg }}
{% endif %} - {% if let Some(msg) = success_message %}
{{ msg }}
{% endif %} +
+ +
+ {% if let Some(msg) = danger_message %} +
{{ msg }}
+ {% endif %} + {% if let Some(msg) = success_message %} +
{{ msg }}
+ {% endif %}

{{ page_title }}

{% block content %} TO_REPLACE {% endblock content %} -
+
+ \ No newline at end of file diff --git a/templates/settings/change_password.html b/templates/settings/change_password.html new file mode 100644 index 0000000..3fed380 --- /dev/null +++ b/templates/settings/change_password.html @@ -0,0 +1,57 @@ +{% extends "base_settings_page.html" %} +{% block content %} +
+
+ + +
+ +
 
+ +
+ + +
+ Please choose a password of at least {{ min_pwd_len }} characters. +
+ +
+ + +
+ +
 
+
 
+
 
+ + +
+ + + +{% endblock content %} \ No newline at end of file