diff --git a/data/resources/gtk/ui/main.ui b/data/resources/gtk/ui/main.ui index 10221b5..79303f7 100644 --- a/data/resources/gtk/ui/main.ui +++ b/data/resources/gtk/ui/main.ui @@ -226,14 +226,13 @@ True False - 5 5 5 5 0 in - + True False @@ -245,7 +244,6 @@ True False start - 5 5 20 Group @@ -266,7 +264,7 @@ 1 - 4 + 5 @@ -275,7 +273,6 @@ True False Group - 5 5 20 True @@ -291,7 +288,6 @@ True False start - 5 5 Secret 0 @@ -339,9 +335,8 @@ True False - 5 5 - 20 + 10 use QR code True @@ -363,7 +358,6 @@ 140 True False - 5 5 0 none @@ -373,8 +367,6 @@ True True Secret - 10 - 5 10 5 5 @@ -412,9 +404,7 @@ True False end - 5 5 - 30 20 5 @@ -452,9 +442,28 @@ 1 - 5 + 6 + + + edit_account_icon_error + True + False + 5 + True + 0 + False + 0 + + + 1 + 4 + + + + + @@ -691,7 +700,7 @@ False 5 True - 23 + 0 False 0 diff --git a/data/resources/style.css b/data/resources/style.css index 3673400..fc5f82e 100644 --- a/data/resources/style.css +++ b/data/resources/style.css @@ -7,13 +7,7 @@ border-color: @error_color; } -/* -* Note: apparently pseudo class :indeterminate -* works (also) with widget state StateFlags::INCONSISTENT. -* -* StateFlags::INDETERMINATE is deprecated. -*/ -.edit_account_input_secret_frame:indeterminate { +.edit_account_input_secret_frame.error { border-color: @error_color; } @@ -90,7 +84,7 @@ font-size: 80%; } -#add_group_icon_error { +#add_group_icon_error, #edit_account_icon_error { color: @error_color; } diff --git a/po/en_GB.po b/po/en_GB.po index f8ddbeb..fd01539 100644 --- a/po/en_GB.po +++ b/po/en_GB.po @@ -181,3 +181,9 @@ msgstr "Name of group" msgid "Name of account" msgstr "Name of account" + +msgid "Group name already exists" +msgstr "Group name already exists" + +msgid "Account name already exists" +msgstr "Account name already exists" diff --git a/po/fr.po b/po/fr.po index 49c0424..75d2cbd 100644 --- a/po/fr.po +++ b/po/fr.po @@ -181,3 +181,9 @@ msgstr "Nom du groupe" msgid "Name of account" msgstr "Nom du compte" + +msgid "Group name already exists" +msgstr "Un groupe existe avec ce nom" + +msgid "Account name already exists" +msgstr "Un compte existe avec ce nom" \ No newline at end of file diff --git a/src/helpers/database.rs b/src/helpers/database.rs index 127ee5b..021e641 100644 --- a/src/helpers/database.rs +++ b/src/helpers/database.rs @@ -155,6 +155,23 @@ impl Database { .map_err(RepositoryError::SqlError) } + pub fn account_exists(connection: &Connection, name: &str, group_id: u32) -> Result> { + let mut stmt = connection.prepare("SELECT id FROM accounts WHERE label = :label AND group_id = :group_id")?; + + stmt.query_row( + named_params! { + ":label": name, + ":group_id": group_id, + }, + |row| { + let account_id: u32 = row.get_unwrap(0); + Ok(account_id) + }, + ) + .optional() + .map_err(RepositoryError::SqlError) + } + pub fn get_group(connection: &Connection, group_id: u32) -> Result { let mut stmt = connection.prepare("SELECT id, name, icon, url, collapsed FROM groups WHERE id = :group_id")?; @@ -509,6 +526,22 @@ mod tests { assert!(result.is_none()); } + #[test] + fn account_exists() { + let mut connection = Connection::open_in_memory().unwrap(); + + runner::run(&mut connection).unwrap(); + + let mut account = Account::new(0, 1, "label", "secret", LOCAL); + let _ = Database::save_account(&connection, &mut account); + + let result = Database::account_exists(&connection, "label", 1).unwrap(); + assert!(result.is_some()); + + let result = Database::account_exists(&connection, "non_existent", 1).unwrap(); + assert!(result.is_none()); + } + #[test] fn save_group_and_accounts() { let mut connection = Connection::open_in_memory().unwrap(); diff --git a/src/ui/add_group.rs b/src/ui/add_group.rs index 6f5d56c..2a812bc 100644 --- a/src/ui/add_group.rs +++ b/src/ui/add_group.rs @@ -5,6 +5,7 @@ use std::io::prelude::*; use std::path::PathBuf; use std::sync::{Arc, Mutex}; +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; use gtk::{Builder, IconSize}; @@ -77,7 +78,7 @@ impl AddGroupWindow { let group_id = group_id.parse().map(Some).unwrap_or(None); if existing_group.is_some() && existing_group != group_id { - self.icon_error.set_label("Group name already exists"); + self.icon_error.set_label(&gettext("Group name already exists")); self.icon_error.set_visible(true); return Err(ValidationError::FieldError("name".to_owned())); } diff --git a/src/ui/edit_account_window.rs b/src/ui/edit_account_window.rs index 2026334..6764c4e 100644 --- a/src/ui/edit_account_window.rs +++ b/src/ui/edit_account_window.rs @@ -1,6 +1,6 @@ use std::sync::{Arc, Mutex}; -use gettextrs::*; +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; use gtk::{Builder, StateFlags}; @@ -28,6 +28,7 @@ pub struct EditAccountWindow { pub save_button: gtk::Button, pub image_dialog: gtk::FileChooserDialog, pub input_secret_frame: gtk::Frame, + pub icon_error: gtk::Label, } impl EditAccountWindow { @@ -43,6 +44,7 @@ impl EditAccountWindow { qr_button: builder.object("qrcode_button").unwrap(), image_dialog: builder.object("file_chooser_dialog").unwrap(), input_secret_frame: builder.object("edit_account_input_secret_frame").unwrap(), + icon_error: builder.object("edit_account_icon_error").unwrap(), } } @@ -55,18 +57,39 @@ impl EditAccountWindow { }); } - fn validate(&self) -> Result<(), ValidationError> { + fn validate(&self, connection: Arc>) -> Result<(), ValidationError> { let name = self.input_name.clone(); let secret = self.input_secret.clone(); let input_secret_frame = self.input_secret_frame.clone(); let mut result: Result<(), ValidationError> = Ok(()); - if name.buffer().text().is_empty() { + fn highlight_name_error(name: >k::Entry) { name.set_primary_icon_name(Some("dialog-error")); let style_context = name.style_context(); style_context.add_class("error"); + } + + if name.buffer().text().is_empty() { + highlight_name_error(&name); result = Err(ValidationError::FieldError("name".to_owned())); + } else { + let group = self.input_group.clone(); + let group_id: u32 = group.active_id().unwrap().as_str().parse().unwrap(); + + let connection = connection.lock().unwrap(); + let existing_account = Database::account_exists(&connection, name.buffer().text().as_str(), group_id); + let existing_account = existing_account.unwrap_or(None); + + let account_id = self.input_account_id.buffer().text(); + let account_id = account_id.parse().map(Some).unwrap_or(None); + + if existing_account.is_some() && existing_account != account_id { + highlight_name_error(&name); + self.icon_error.set_label(&gettext("Account name already exists")); + self.icon_error.set_visible(true); + result = Err(ValidationError::FieldError("name".to_owned())); + } } let buffer = secret.buffer().unwrap(); @@ -78,16 +101,19 @@ impl EditAccountWindow { if secret_value.is_empty() { let style_context = input_secret_frame.style_context(); - style_context.set_state(StateFlags::INCONSISTENT); + style_context.add_class("error"); result = Err(ValidationError::FieldError("secret".to_owned())); } else { let stripped = Self::strip_secret(&secret_value); + let style_context = input_secret_frame.style_context(); + match Account::generate_time_based_password(stripped.as_str()) { + Ok(_) if style_context.has_class("error") => buffer.set_text(&secret_value), Ok(_) => buffer.set_text(&stripped), Err(error_key) => { error!("{}", error_key.error()); - let style_context = input_secret_frame.style_context(); - style_context.set_state(StateFlags::INCONSISTENT); + + style_context.add_class("error"); result = Err(ValidationError::FieldError("secret".to_owned())); } } @@ -102,6 +128,8 @@ impl EditAccountWindow { let group = self.input_group.clone(); let input_secret_frame = self.input_secret_frame.clone(); + self.icon_error.set_label(""); + name.set_primary_icon_name(None); let style_context = name.style_context(); style_context.remove_class("error"); @@ -152,12 +180,15 @@ impl EditAccountWindow { let dialog = self.image_dialog.clone(); let input_secret = self.input_secret.clone(); let save_button = self.save_button.clone(); + let input_secret_frame = self.input_secret_frame.clone(); qr_button.connect_clicked(clone!( #[strong] save_button, #[strong] input_secret, + #[strong] + input_secret_frame, #[strong(rename_to = w)] self, move |_| { @@ -178,25 +209,32 @@ impl EditAccountWindow { #[strong] input_secret, #[strong] + input_secret_frame, + #[strong] w, async move { let result = QrCode::process_qr_code(path.to_str().unwrap().to_owned()).await; + save_button.set_sensitive(true); + let style_context = input_secret_frame.style_context(); + match result { Valid(qr_code) => { + w.reset_errors(); let buffer = input_secret.buffer().unwrap(); + style_context.remove_class("error"); buffer.set_text(qr_code.extract()); } Invalid(qr_code) => { let buffer = input_secret.buffer().unwrap(); - buffer.set_text(&gettext(qr_code)); - } - }; - w.reset_errors(); - save_button.set_sensitive(true); + w.icon_error.set_label(&gettext(qr_code)); + w.icon_error.set_visible(true); - w.validate() + style_context.add_class("error"); + buffer.set_text(""); + } + }; } )); } @@ -230,13 +268,13 @@ impl EditAccountWindow { move |_| { edit_account.reset_errors(); - if let Ok(()) = edit_account.validate() { + if let Ok(()) = edit_account.validate(connection.clone()) { let name = edit_account.input_name.clone(); let secret = edit_account.input_secret.clone(); let account_id = edit_account.input_account_id.clone(); let group = edit_account.input_group.clone(); let name: String = name.buffer().text(); - let group_id: u32 = group.active_id().unwrap().as_str().to_owned().parse().unwrap(); + let group_id: u32 = group.active_id().unwrap().as_str().parse().unwrap(); let secret: String = { let buffer = secret.buffer().unwrap(); let (start, end) = buffer.bounds();