diff --git a/Cargo.lock b/Cargo.lock index 62e2d72..28e0075 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -393,6 +393,7 @@ dependencies = [ "include_dir", "log", "mime_guess", + "rand", "serde", "serde_json", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index ec1afb6..c8fb925 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,4 +21,5 @@ uuid = { version = "0.8.2", features = ["v4"] } mime_guess = "2.0.4" askama = "0.11.1" futures-util = "0.3.21" -urlencoding = "2.1.0" \ No newline at end of file +urlencoding = "2.1.0" +rand = "0.8.5" \ No newline at end of file diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index 43b7217..8260951 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -18,9 +18,6 @@ pub struct LoginRequest { pub password: String, } -#[derive(Debug)] -pub struct ChangePasswordResult(pub bool); - #[derive(Message)] #[rtype(GetUserResult)] pub struct GetUserRequest(pub UserID); @@ -50,6 +47,16 @@ pub struct ChangePasswordRequest { pub temporary: bool, } +#[derive(Debug)] +pub struct ChangePasswordResult(pub bool); + +#[derive(Debug)] +pub struct UpdateUserResult(pub bool); + +#[derive(Message)] +#[rtype(UpdateUserResult)] +pub struct UpdateUserRequest(pub User); + pub struct UsersActor { manager: EntityManager, } @@ -119,4 +126,18 @@ impl Handler for UsersActor { fn handle(&mut self, _msg: GetAllUsersRequest, _ctx: &mut Self::Context) -> Self::Result { MessageResult(GetAllUsersResult(self.manager.cloned())) } +} + +impl Handler for UsersActor { + type Result = MessageResult; + + fn handle(&mut self, msg: UpdateUserRequest, _ctx: &mut Self::Context) -> Self::Result { + MessageResult(UpdateUserResult(match self.manager.update_or_replace(msg.0) { + Ok(_) => true, + Err(e) => { + log::error!("Failed to update user information! {:?}", e); + false + } + })) + } } \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs index a20e0cc..6a1c7d9 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -37,4 +37,7 @@ pub const LOGIN_ROUTE: &str = "/login"; /// Bruteforce protection pub const KEEP_FAILED_LOGIN_ATTEMPTS_FOR: u64 = 3600; pub const MAX_FAILED_LOGIN_ATTEMPTS: usize = 15; -pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); \ No newline at end of file +pub const FAIL_LOGIN_ATTEMPT_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); + +/// Temporary password length +pub const TEMPORARY_PASSWORDS_LEN: usize = 20; \ No newline at end of file diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs index affe0f1..aa1440f 100644 --- a/src/controllers/admin_controller.rs +++ b/src/controllers/admin_controller.rs @@ -6,10 +6,12 @@ use askama::Template; use crate::actors::users_actor; use crate::actors::users_actor::UsersActor; +use crate::constants::TEMPORARY_PASSWORDS_LEN; use crate::controllers::settings_controller::BaseSettingsPage; -use crate::data::client::{Client, ClientManager}; +use crate::data::client::{Client, ClientID, ClientManager}; use crate::data::current_user::CurrentUser; -use crate::data::user::User; +use crate::data::user::{hash_password, User, UserID}; +use crate::utils::string_utils::rand_str; #[derive(Template)] #[template(path = "settings/clients_list.html")] @@ -46,15 +48,85 @@ pub async fn clients_route(user: CurrentUser, clients: web::Data) }.render().unwrap()) } -pub async fn users_route(user: CurrentUser, users: web::Data>) -> impl Responder { +#[derive(serde::Deserialize, Debug)] +pub struct UpdateUserQuery { + uid: UserID, + username: String, + first_name: String, + last_name: String, + email: String, + gen_new_password: Option, + enabled: Option, + admin: Option, + grant_type: String, + granted_clients: String, +} + +pub async fn users_route(user: CurrentUser, users: web::Data>, update_query: Option>) -> impl Responder { + let mut danger = None; + let mut success = None; + + if let Some(update) = update_query { + let current_user: Option = users.send(users_actor::FindUserByUsername(update.username.to_string())) + .await.unwrap().0; + let is_creating = current_user.is_none(); + + let mut user = current_user.unwrap_or_default(); + user.uid = update.0.uid; + user.username = update.0.username; + user.first_name = update.0.first_name; + user.last_name = update.0.last_name; + user.email = update.0.email; + user.enabled = update.0.enabled.is_some(); + user.admin = update.0.admin.is_some(); + + user.authorized_clients = match update.0.grant_type.as_str() { + "all_clients" => None, + "custom_clients" => Some(update.0.granted_clients.split(',') + .map(|c| ClientID(c.to_string())) + .collect::>()), + _ => Some(Vec::new()) + }; + + let new_password = match update.0.gen_new_password.is_some() { + false => None, + true => { + let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN); + user.password = hash_password(&temp_pass) + .expect("Failed to hash password"); + user.need_reset_password = true; + Some(temp_pass) + } + }; + + let res = users.send(users_actor::UpdateUserRequest(user.clone())).await.unwrap().0; + + if !res { + danger = Some(match is_creating { + true => "Failed to create user!", + false => "Failed to update user!" + }.to_string()) + } else { + success = Some(match is_creating { + true => format!("User {} was successfully updated!", user.full_name()), + false => format!("Failed to update {}'s account!", user.full_name()) + }.to_string()); + + if let Some(pass) = new_password { + danger = Some(format!("{}'s temporary time password is {}", user.full_name(), pass)); + } + } + } + + let users = users.send(users_actor::GetAllUsersRequest).await.unwrap().0; HttpResponse::Ok().body(UsersListTemplate { _parent: BaseSettingsPage::get( "Users list", &user, - None, - None, + danger, + success, ), users, }.render().unwrap()) diff --git a/src/data/entity_manager.rs b/src/data/entity_manager.rs index 94e2fff..6443c05 100644 --- a/src/data/entity_manager.rs +++ b/src/data/entity_manager.rs @@ -91,4 +91,21 @@ impl EntityManager pub fn cloned(&self) -> Vec { self.list.clone() } + + pub fn update_or_replace(&mut self, entry: E) -> Res { + let mut found = false; + for i in &mut self.list { + if i == &entry { + *i = entry.clone(); + found = true; + break; + } + } + + if !found { + self.list.push(entry); + } + + self.save() + } } diff --git a/src/data/user.rs b/src/data/user.rs index 7c9def5..70a33ad 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -22,6 +22,10 @@ pub struct User { } impl User { + pub fn full_name(&self) -> String { + format!("{} {}", self.first_name, self.last_name) + } + pub fn can_access_app(&self, id: &ClientID) -> bool { match &self.authorized_clients { None => true, diff --git a/src/main.rs b/src/main.rs index d777cf6..2a29d8a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -116,6 +116,7 @@ async fn main() -> std::io::Result<()> { .to(|| async { HttpResponse::Found().append_header(("Location", "/settings")).finish() })) .route("/admin/clients", web::get().to(admin_controller::clients_route)) .route("/admin/users", web::get().to(admin_controller::users_route)) + .route("/admin/users", web::post().to(admin_controller::users_route)) .route("/admin/create_user", web::get().to(admin_controller::create_user)) // Admin API diff --git a/src/utils/mod.rs b/src/utils/mod.rs index b3d702f..fd21bed 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,3 +1,4 @@ pub mod err; pub mod time; -pub mod network_utils; \ No newline at end of file +pub mod network_utils; +pub mod string_utils; \ No newline at end of file diff --git a/src/utils/string_utils.rs b/src/utils/string_utils.rs new file mode 100644 index 0000000..237d8c2 --- /dev/null +++ b/src/utils/string_utils.rs @@ -0,0 +1,11 @@ +use rand::distributions::Alphanumeric; +use rand::Rng; + +/// Generate a random string of a given size +pub fn rand_str(len: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .map(char::from) + .take(len) + .collect() +} diff --git a/templates/settings/edit_user.html b/templates/settings/edit_user.html index 7eb69e4..4f2585b 100644 --- a/templates/settings/edit_user.html +++ b/templates/settings/edit_user.html @@ -144,7 +144,7 @@ clientsSelectorEl.style.display = radioBtn.checked ? "block" : "none"; } refreshDisplayAuthorizedClients(); - document.querySelectorAll("input[name=granted_clients]").forEach(el=> { + document.querySelectorAll("input[name=grant_type]").forEach(el=> { el.addEventListener("change", refreshDisplayAuthorizedClients) })