diff --git a/Cargo.toml b/Cargo.toml index 18bfcd88..1e6535b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,9 +17,12 @@ futures = {version = "0.3.5", features = ["thread-pool"]} futures-executor = "0.3.5" gdk = "0.12.1" gio = "0.8.1" +log = "0.4.8" +log4rs = "0.12.0" rusqlite = "0.23.1" -serde_json = "1.0.55" serde = { version = "1.0.111", features = ["derive"] } +serde_json = "1.0.55" +serde_yaml = "0.8.13" totp-rs = "0.2.6" [dependencies.gtk] diff --git a/README.md b/README.md index 49880e1f..87968309 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Download from the [release](https://github.com/grumlimited/authenticator-rs/rele make target/release/authenticator-rs - $HOME/.cargo/bin/authenticator-rust + ./target/release/authenticator-rs ## Running (dev) diff --git a/data/log4rs.yaml b/data/log4rs.yaml new file mode 100644 index 00000000..7f3aff2d --- /dev/null +++ b/data/log4rs.yaml @@ -0,0 +1,14 @@ +refresh_rate: 30 seconds + +appenders: + stdout: + kind: console + +root: + level: info + appenders: + - stdout + +loggers: + authenticator_rs: + level: debug diff --git a/data/uk.co.grumlimited.authenticator-rs.xml b/data/uk.co.grumlimited.authenticator-rs.xml index 41371b7b..8e3a37b5 100644 --- a/data/uk.co.grumlimited.authenticator-rs.xml +++ b/data/uk.co.grumlimited.authenticator-rs.xml @@ -3,5 +3,6 @@ data/uk.co.grumlimited.authenticator-rs.svg data/style.css + data/log4rs.yaml diff --git a/src/helpers/config_manager.rs b/src/helpers/config_manager.rs index a61f88d6..e56ea0ed 100644 --- a/src/helpers/config_manager.rs +++ b/src/helpers/config_manager.rs @@ -20,13 +20,6 @@ pub enum LoadError { } impl ConfigManager { - pub fn _log4rs() -> std::path::PathBuf { - let mut path = ConfigManager::path(); - path.push("log4rs.yaml"); - - path - } - fn db_path() -> std::path::PathBuf { let mut path = ConfigManager::path(); path.push("authenticator.db"); @@ -54,7 +47,9 @@ impl ConfigManager { let conn = conn.lock().unwrap(); - let mut stmt = conn.prepare("SELECT id, name FROM groups").unwrap(); + let mut stmt = conn + .prepare("SELECT id, name FROM groups ORDER BY LOWER(name)") + .unwrap(); stmt.query_map(params![], |row| { let id: u32 = row.get(0)?; @@ -75,27 +70,6 @@ impl ConfigManager { .map_err(|e| LoadError::DbError(format!("{:?}", e))) } - fn _create_group( - conn: Arc>, - group_name: &str, - ) -> Result { - let conn = conn.lock().unwrap(); - - conn.execute("INSERT INTO groups (name) VALUES (?1)", params![group_name]) - .map_err(|e| LoadError::DbError(format!("{:?}", e))) - .unwrap(); - - let mut stmt = conn.prepare("SELECT last_insert_rowid()").unwrap(); - - stmt.query_row(NO_PARAMS, |row| row.get::(0)) - .map(|id| AccountGroup { - id, - name: group_name.to_owned(), - ..Default::default() - }) - .map_err(|e| LoadError::DbError(format!("{:?}", e))) - } - pub fn update_group( conn: Arc>, group: &AccountGroup, @@ -274,8 +248,9 @@ impl ConfigManager { } fn get_accounts(conn: &Connection, group_id: u32) -> Result, rusqlite::Error> { - let mut stmt = - conn.prepare("SELECT id, label, secret FROM accounts WHERE group_id = ?1")?; + let mut stmt = conn.prepare( + "SELECT id, label, secret FROM accounts WHERE group_id = ?1 ORDER BY LOWER(label)", + )?; stmt.query_map(params![group_id], |row| { let id: u32 = row.get(0)?; @@ -294,10 +269,8 @@ impl ConfigManager { mod tests { use super::ConfigManager; use rusqlite::Connection; - use std::path::Path; use crate::model::{Account, AccountGroup}; - use async_std::task; use std::sync::{Arc, Mutex}; #[test] @@ -306,7 +279,7 @@ mod tests { let _ = { let conn = conn.clone(); - ConfigManager::init_tables(conn); + ConfigManager::init_tables(conn).expect("boom!"); }; let mut group = AccountGroup::new(0, "new group", vec![]); @@ -352,7 +325,7 @@ mod tests { let _ = { let conn = conn.clone(); - ConfigManager::init_tables(conn); + ConfigManager::init_tables(conn).expect("boom!"); }; let mut group = AccountGroup::new(0, "new group", vec![]); @@ -382,12 +355,13 @@ mod tests { let _ = { let conn = conn.clone(); - ConfigManager::init_tables(conn); + ConfigManager::init_tables(conn).expect("boom!"); }; + let mut group = AccountGroup::new(0, "existing_group2", vec![]); let group = { let conn = conn.clone(); - ConfigManager::_create_group(conn, "existing_group2").unwrap() + ConfigManager::save_group(conn, &mut group).unwrap() }; let mut account = Account::new(0, group.id, "label", "secret"); @@ -409,21 +383,42 @@ mod tests { } #[test] - fn get_or_create_group_with_new_group() { + fn save_group_ordering() { let conn = Arc::new(Mutex::new(Connection::open_in_memory().unwrap())); - let _ = { + { let conn = conn.clone(); - ConfigManager::init_tables(conn); + ConfigManager::init_tables(conn).expect("boom!"); }; - let group = { + { + let conn = conn.clone(); + let conn2 = conn.clone(); + let conn3 = conn.clone(); + let mut group = AccountGroup::new(0, "bbb", vec![]); + ConfigManager::save_group(conn, &mut group).unwrap(); + + let mut account1 = Account::new(0, group.id, "hhh", "secret3"); + ConfigManager::save_account(conn2, &mut account1).expect("boom!"); + let mut account2 = Account::new(0, group.id, "ccc", "secret3"); + ConfigManager::save_account(conn3, &mut account2).expect("boom!"); + }; + + { let conn = conn.clone(); - ConfigManager::_create_group(conn, "existing_group2").unwrap() + let mut group = AccountGroup::new(0, "AAA", vec![]); + ConfigManager::save_group(conn, &mut group).expect("boom!"); }; - assert!(group.id > 0); - assert_eq!("existing_group2", group.name); + let results = ConfigManager::load_account_groups(conn).unwrap(); + + //groups in order + assert_eq!("AAA", results.get(0).unwrap().name); + assert_eq!("bbb", results.get(1).unwrap().name); + + //accounts in order + assert_eq!("ccc", results.get(1).unwrap().entries.get(0).unwrap().label); + assert_eq!("hhh", results.get(1).unwrap().entries.get(1).unwrap().label); } #[test] @@ -432,7 +427,7 @@ mod tests { let _ = { let conn = conn.clone(); - ConfigManager::init_tables(conn); + ConfigManager::init_tables(conn).expect("boom!"); }; let mut account = Account::new(0, 0, "label", "secret"); diff --git a/src/main.rs b/src/main.rs index 310df038..0657afee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,6 +11,8 @@ use crate::model::AccountGroup; use crate::ui::{AccountsWindow, AddGroupWindow, EditAccountWindow}; use gio::prelude::*; use gtk::prelude::*; +use log4rs::config::Config; +use log4rs::file::{Deserializers, RawConfig}; use rusqlite::Connection; use std::cell::RefCell; use std::sync::{Arc, Mutex}; @@ -19,6 +21,8 @@ mod helpers; mod model; mod ui; +use log::info; + const NAMESPACE: &str = "uk.co.grumlimited.authenticator-rs"; const NAMESPACE_PREFIX: &str = "/uk/co/grumlimited/authenticator-rs"; @@ -27,7 +31,7 @@ fn main() { .expect("Initialization failed..."); let resource = { - match gio::Resource::load(format!("/usr/share/{}/{}.gresource", NAMESPACE, NAMESPACE)) { + match gio::Resource::load(format!("data/{}.gresource", NAMESPACE)) { Ok(resource) => resource, Err(_) => gio::Resource::load(format!("data/{}.gresource", NAMESPACE)).unwrap(), } @@ -44,6 +48,8 @@ fn main() { &provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, ); + + configure_logging(); }); application.connect_activate(|app| { @@ -88,7 +94,36 @@ fn main() { } AccountsWindow::delete_buttons_actions(gui, connection); + + info!("Authenticator RS initialised"); }); application.run(&[]); } + +/** +* Loads log4rs yaml config from gResource. +* And in the most convoluted possible way, feeds it to Log4rs. +*/ +fn configure_logging() { + let log4rs_yaml = gio::functions::resources_lookup_data( + format!("{}/{}", NAMESPACE_PREFIX, "log4rs.yaml").as_str(), + gio::ResourceLookupFlags::NONE, + ) + .unwrap(); + let log4rs_yaml = log4rs_yaml.to_vec(); + let log4rs_yaml = String::from_utf8(log4rs_yaml).unwrap(); + + // log4rs-0.12.0/src/file.rs#592 + let config = serde_yaml::from_str::(log4rs_yaml.as_str()).unwrap(); + let (appenders, _) = config.appenders_lossy(&Deserializers::default()); + + // log4rs-0.12.0/src/priv_file.rs#deserialize(config: &RawConfig, deserializers: &Deserializers)#186 + let config = Config::builder() + .appenders(appenders) + .loggers(config.loggers()) + .build(config.root()) + .unwrap(); + + log4rs::init_config(config).unwrap(); +} diff --git a/src/main_window.rs b/src/main_window.rs index 7f0bad63..ed84362b 100644 --- a/src/main_window.rs +++ b/src/main_window.rs @@ -23,7 +23,7 @@ pub struct MainWindow { pub edit_account_window: ui::EditAccountWindow, pub accounts_window: ui::AccountsWindow, pub add_group: ui::AddGroupWindow, - pool: ThreadPool, + pub pool: ThreadPool, state: Rc>, } @@ -235,10 +235,18 @@ impl MainWindow { }); } + { + let titlebar = gtk::HeaderBarBuilder::new() + .decoration_layout(":") + .title("About") + .build(); + + let popup = self.popup.clone(); + popup.set_titlebar(Some(&titlebar)); + } { let popup = self.popup.clone(); about_button.connect_clicked(move |_| { - popup.set_title("About"); popover.set_visible(false); popup.set_visible(true); popup.show_all(); @@ -365,6 +373,10 @@ impl MainWindow { .append(entry_id, group.name.as_str()); }); + let first_entry = groups.get(0).map(|e| format!("{}", e.id)); + let first_entry = first_entry.as_deref(); + edit_account_window.input_group.set_active_id(first_entry); + edit_account_window.input_account_id.set_text("0"); edit_account_window.input_name.set_text(""); diff --git a/src/mainwindow.glade b/src/mainwindow.glade index a5b43e51..0a4445e5 100644 --- a/src/mainwindow.glade +++ b/src/mainwindow.glade @@ -536,6 +536,8 @@ True center-on-parent True + True + True False main_window main_window diff --git a/src/model/account.rs b/src/model/account.rs index 05e4266c..b5590f1c 100644 --- a/src/model/account.rs +++ b/src/model/account.rs @@ -5,6 +5,7 @@ use base32::Alphabet::RFC4648; use gtk::prelude::*; use gtk::{Align, Orientation}; +use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Default, PartialEq)] pub struct Account { @@ -21,7 +22,10 @@ pub struct AccountWidgets { pub grid: gtk::Grid, pub edit_button: gtk::Button, pub delete_button: gtk::Button, + pub copy_button: Arc>, pub popover: gtk::PopoverMenu, + pub edit_copy_img: Arc>, + pub dialog_ok_img: Arc>, totp_label: gtk::Label, totp_secret: String, } @@ -68,12 +72,13 @@ impl Account { .xalign(0.0) .build(); - let image = gtk::ImageBuilder::new().icon_name("edit-copy").build(); + let edit_copy_img = gtk::ImageBuilder::new().icon_name("edit-copy").build(); + let dialog_ok_img = gtk::ImageBuilder::new().icon_name("dialog-ok").build(); let copy_button = gtk::ButtonBuilder::new() .margin_start(5) .margin_end(5) - .image(&image) + .image(&edit_copy_img) .always_show_image(true) .build(); @@ -146,6 +151,9 @@ impl Account { grid, edit_button, delete_button, + copy_button: Arc::new(Mutex::new(copy_button)), + edit_copy_img: Arc::new(Mutex::new(edit_copy_img)), + dialog_ok_img: Arc::new(Mutex::new(dialog_ok_img)), popover: popover_clone, totp_label: totp_label_clone2, totp_secret: self.secret.clone(), diff --git a/src/ui/accounts_window.rs b/src/ui/accounts_window.rs index 5a5e5b7d..6cc536e0 100644 --- a/src/ui/accounts_window.rs +++ b/src/ui/accounts_window.rs @@ -148,6 +148,35 @@ impl AccountsWindow { let gui = gui.clone(); + { + let (tx, rx) = glib::MainContext::channel::(glib::PRIORITY_DEFAULT); + + let dialog_ok_img = account_widgets.dialog_ok_img.clone(); + let edit_copy_img = account_widgets.edit_copy_img.clone(); + let copy_button_1 = account_widgets.copy_button.clone(); + let copy_button_2 = account_widgets.copy_button.clone(); + + let pool = gui.pool.clone(); + + rx.attach(None, move |_| { + let edit_copy_img = edit_copy_img.lock().unwrap(); + let edit_copy_img = edit_copy_img.deref(); + let copy_button = copy_button_2.lock().unwrap(); + copy_button.set_image(Some(edit_copy_img)); + glib::Continue(true) + }); + + let copy_button = copy_button_1.lock().unwrap(); + copy_button.connect_clicked(move |b| { + let dialog_ok_img = dialog_ok_img.lock().unwrap(); + let dialog_ok_img = dialog_ok_img.deref(); + b.set_image(Some(dialog_ok_img)); + + let tx = tx.clone(); + pool.spawn_ok(times_up(tx, 2000)); + }); + } + account_widgets.edit_button.connect_clicked(move |_| { let groups = { let connection = connection.clone(); @@ -238,6 +267,19 @@ impl AccountsWindow { } } +use glib::Sender; +use std::ops::Deref; +use std::{thread, time}; + +/** +* Sleeps for some time then messages end of wait, so that copy button +* gets its default image restored. +*/ +async fn times_up(tx: Sender, wait_ms: u64) { + thread::sleep(time::Duration::from_millis(wait_ms)); + tx.send(true).expect("Couldn't send data to channel"); +} + #[cfg(test)] mod tests { use super::*;