diff --git a/cosmic-settings/src/pages/system/users/mod.rs b/cosmic-settings/src/pages/system/users/mod.rs index 86e5314..74fb6f3 100644 --- a/cosmic-settings/src/pages/system/users/mod.rs +++ b/cosmic-settings/src/pages/system/users/mod.rs @@ -32,8 +32,9 @@ pub struct User { id: u64, profile_icon: Option, full_name: String, - password: String, username: String, + password: String, + password_confirm: String, full_name_edit: bool, password_edit: bool, username_edit: bool, @@ -43,13 +44,13 @@ pub struct User { #[derive(Clone, Debug, PartialEq, Eq)] pub enum EditorField { FullName, - Password, Username, } #[derive(Clone, Debug)] pub enum Dialog { AddNewUser(User), + UpdatePassword(User), } #[derive(Clone, Debug)] @@ -62,8 +63,11 @@ pub struct Page { dialog: Option, default_icon: icon::Handle, password_label: String, + password_confirm_label: String, username_label: String, fullname_label: String, + password_hidden: bool, + password_confirm_hidden: bool, } impl Default for Page { @@ -77,8 +81,11 @@ impl Default for Page { dialog: None, default_icon: icon::from_path(PathBuf::from(DEFAULT_ICON_FILE)), password_label: crate::fl!("password"), + password_confirm_label: crate::fl!("password-confirm"), username_label: crate::fl!("username"), fullname_label: crate::fl!("full-name"), + password_hidden: true, + password_confirm_hidden: true, } } } @@ -99,6 +106,9 @@ pub enum Message { SelectedUserDelete(u64), SelectedUserSetAdmin(u64, bool), ToggleEdit(usize, EditorField), + TogglePasswordVisibility, + TogglePasswordConfirmVisibility, + SaveNewPassword(User), } impl From for crate::app::Message { @@ -159,15 +169,35 @@ impl page::Page for Page { ); let password_input = widget::container( - widget::text_input("", &user.password) - .password() - .label(&self.password_label) - .on_input(|value| { - Message::Dialog(Some(Dialog::AddNewUser(User { - password: value, - ..user.clone() - }))) - }), + widget::secure_input( + "", + &user.password, + Some(Message::TogglePasswordVisibility), + self.password_hidden, + ) + .label(&self.password_label) + .on_input(|value| { + Message::Dialog(Some(Dialog::AddNewUser(User { + password: value, + ..user.clone() + }))) + }), + ); + + let password_confirm_input = widget::container( + widget::secure_input( + "", + &user.password_confirm, + Some(Message::TogglePasswordConfirmVisibility), + self.password_confirm_hidden, + ) + .label(&self.password_confirm_label) + .on_input(|value| { + Message::Dialog(Some(Dialog::AddNewUser(User { + password_confirm: value, + ..user.clone() + }))) + }), ); let admin_toggler = widget::toggler(user.is_admin).on_toggle(|value| { @@ -184,9 +214,16 @@ impl page::Page for Page { let complete_maybe = if !username_valid && !user.username.is_empty() { validation_msg = fl!("invalid-username"); None + } else if user.password != user.password_confirm + && user.password != "" + && user.password_confirm != "" + { + validation_msg = fl!("password-mismatch"); + None } else if user.full_name.is_empty() || user.username.is_empty() || user.password.is_empty() + || user.password_confirm.is_empty() { None } else { @@ -212,6 +249,7 @@ impl page::Page for Page { .add(full_name_input) .add(username_input) .add(password_input) + .add(password_confirm_input) .add( row::with_capacity(3) .push( @@ -232,6 +270,73 @@ impl page::Page for Page { .tertiary_action(widget::text::body(validation_msg)) .apply(Element::from) } + + Dialog::UpdatePassword(user) => { + let password_input = widget::container( + widget::secure_input( + "", + &user.password, + Some(Message::TogglePasswordVisibility), + self.password_hidden, + ) + .label(&self.password_label) + .on_input(|value| { + Message::Dialog(Some(Dialog::UpdatePassword(User { + password: value, + ..user.clone() + }))) + }), + ); + + let password_confirm_input = widget::container( + widget::secure_input( + "", + &user.password_confirm, + Some(Message::TogglePasswordConfirmVisibility), + self.password_confirm_hidden, + ) + .label(&self.password_confirm_label) + .on_input(|value| { + Message::Dialog(Some(Dialog::UpdatePassword(User { + password_confirm: value, + ..user.clone() + }))) + }), + ); + + // validation + let mut validation_msg = String::new(); + let complete_maybe = if user.password != user.password_confirm + && user.password != "" + && user.password_confirm != "" + { + validation_msg = fl!("password-mismatch"); + None + } else if user.password.is_empty() || user.password_confirm.is_empty() { + None + } else { + Some(Message::SaveNewPassword(user.clone())) + }; + + let save_button = widget::button::suggested(fl!("save")) + .on_press_maybe(complete_maybe) + .apply(Element::from); + + let cancel_button = + widget::button::standard(fl!("cancel")).on_press(Message::Dialog(None)); + + widget::dialog() + .title(fl!("change-password")) + .control( + widget::ListColumn::default() + .add(password_input) + .add(password_confirm_input), + ) + .primary_action(save_button) + .secondary_action(cancel_button) + .tertiary_action(widget::text::body(validation_msg)) + .apply(Element::from) + } }; dialog_element.map(crate::pages::Message::User).into() @@ -293,6 +398,7 @@ impl Page { username: String::from(user.username), full_name: String::from(user.full_name), password: String::new(), + password_confirm: String::new(), full_name_edit: false, password_edit: false, username_edit: false, @@ -379,7 +485,6 @@ impl Page { if let Some(user) = self.users.get_mut(id) { match field { EditorField::FullName => user.full_name = value, - EditorField::Password => user.password = value, EditorField::Username => user.username = value, } } @@ -389,12 +494,18 @@ impl Page { if let Some(user) = self.users.get_mut(id) { match field { EditorField::FullName => user.full_name_edit = !user.full_name_edit, - EditorField::Password => user.password_edit = !user.password_edit, EditorField::Username => user.username_edit = !user.username_edit, } } } + Message::TogglePasswordVisibility => { + self.password_hidden = !self.password_hidden; + } + Message::TogglePasswordConfirmVisibility => { + self.password_confirm_hidden = !self.password_confirm_hidden; + } + Message::ApplyEdit(id, field) => { if let Some(user) = self.users.get_mut(id) { let uid = user.id; @@ -420,34 +531,6 @@ impl Page { .discard(); } - EditorField::Password => { - let password = std::mem::take(&mut user.password); - - return cosmic::Task::future(async move { - let Ok(conn) = zbus::Connection::system().await else { - return; - }; - - let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await - else { - return; - }; - - match request_permission_on_denial(&conn, || { - user.set_password(&password, "") - }) - .await - { - Err(why) => { - tracing::error!(?why, "failed to set password"); - } - - Ok(_) => (), - } - }) - .discard(); - } - EditorField::Username => { let username = user.username.clone(); return cosmic::Task::future(async move { @@ -471,6 +554,34 @@ impl Page { } } + Message::SaveNewPassword(user) => { + self.dialog = None; + + let uid = user.id; + let password = user.password; + + return cosmic::Task::future(async move { + let Ok(conn) = zbus::Connection::system().await else { + return; + }; + + let Ok(user) = accounts_zbus::UserProxy::from_uid(&conn, uid).await else { + return; + }; + + match request_permission_on_denial(&conn, || user.set_password(&password, "")) + .await + { + Err(why) => { + tracing::error!(?why, "failed to set password"); + } + + Ok(_) => (), + } + }) + .discard(); + } + Message::LoadPage(uid, users) => { self.current_user_id = uid; self.users = users; @@ -514,6 +625,8 @@ impl Page { } Message::Dialog(dialog) => { + self.password_hidden = true; + self.password_confirm_hidden = true; self.dialog = dialog; } @@ -617,13 +730,9 @@ fn user_list() -> Section { .on_input(move |name| Message::Edit(idx, EditorField::Username, name)) .on_submit(move |_| Message::ApplyEdit(idx, EditorField::Username)); - let password = - widget::editable_input("", &user.password, user.password_edit, move |_| { - Message::ToggleEdit(idx, EditorField::Password) - }) - .on_input(move |pass| Message::Edit(idx, EditorField::Password, pass)) - .on_submit(move |_| Message::ApplyEdit(idx, EditorField::Password)) - .password(); + let password = widget::button::standard(fl!("change-password")) + .on_press(Message::Dialog(Some(Dialog::UpdatePassword(user.clone())))) + .apply(Element::from); let fullname = widget::editable_input( "", diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 16d320f..b92e27a 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -30,6 +30,7 @@ network-and-wireless = Network & Wireless no-networks = No networks have been found. no-vpn = No VPN connections available. password = Password +password-confirm = Confirm Password remove = Remove settings = Settings username = Username @@ -888,6 +889,9 @@ administrator = Administrator .desc = Administrators can change settings for all users, add and remove other users. add-user = Add user +change-password = Change password remove-user = Remove user full-name = Full name -invalid-username = Invalid username +invalid-username = Invalid username. +password-mismatch = Password and confirmation must match. +save = Save