diff --git a/src/app/browser/window/tab/item/identity/gemini.rs b/src/app/browser/window/tab/item/identity/gemini.rs index 16addca5..7a257b73 100644 --- a/src/app/browser/window/tab/item/identity/gemini.rs +++ b/src/app/browser/window/tab/item/identity/gemini.rs @@ -86,7 +86,7 @@ impl Gemini { .gemini .auth .database - .records(None) + .records_scope(None) .unwrap() .iter() .filter(|this| this.profile_identity_gemini_id == identity.id) @@ -100,7 +100,7 @@ impl Gemini { .gemini .auth .database - .records(Some(auth_url.as_str())) + .records_scope(Some(auth_url.as_str())) .unwrap() .iter() .filter(|this| this.profile_identity_gemini_id == identity.id) @@ -162,7 +162,8 @@ impl Gemini { } // Remove all identity auths for `auth_uri` None => { - if let Err(reason) = profile.identity.gemini.auth.remove(auth_url.as_str()) + if let Err(reason) = + profile.identity.gemini.auth.remove_scope(auth_url.as_str()) { todo!("{}", reason.to_string()) }; diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs index f0f8ca02..1b66e38b 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form.rs @@ -1,8 +1,10 @@ +mod drop; mod file; pub mod list; mod name; mod save; +use drop::Drop; use file::File; use list::{item::value::Value, List}; use name::Name; @@ -15,6 +17,7 @@ use std::rc::Rc; pub struct Form { // pub action: Rc, + // pub drop: Rc, pub file: Rc, pub list: Rc, pub name: Rc, @@ -28,6 +31,7 @@ impl Form { /// Create new `Self` pub fn new(profile: Rc, action: Rc) -> Self { // Init components + let drop = Rc::new(Drop::new(profile.clone(), action.clone())); let file = Rc::new(File::new(action.clone())); let list = Rc::new(List::new()); let name = Rc::new(Name::new(action.clone())); @@ -39,10 +43,12 @@ impl Form { gobject.append(&list.gobject); gobject.append(&name.gobject); gobject.append(&file.gobject); + gobject.append(&drop.gobject); gobject.append(&save.gobject); // Connect events list.on_select({ + let drop = drop.clone(); let file = file.clone(); let name = name.clone(); let save = save.clone(); @@ -54,12 +60,14 @@ impl Form { // Change file choose button visibility file.update(matches!(item, Value::ImportPem)); - // Change export button visibility by update it holder value + // Change other components visibility by update it holder value match item { Value::ProfileIdentityGeminiId(value) => { + drop.update(Some(value)); save.update(Some(value)); } _ => { + drop.update(None); save.update(None); } } @@ -72,11 +80,12 @@ impl Form { // Return activated `Self` Self { // action, - gobject, + // drop, file, list, name, // save, + gobject, } } diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/drop.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/drop.rs new file mode 100644 index 00000000..1413d24f --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/drop.rs @@ -0,0 +1,128 @@ +use super::Action; +use crate::profile::Profile; +use adw::{ + prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, + AlertDialog, ResponseAppearance, +}; +use gtk::{ + prelude::{ButtonExt, WidgetExt}, + Button, +}; +use std::{cell::RefCell, rc::Rc}; + +// Defaults + +const LABEL: &str = "Delete"; +const TOOLTIP_TEXT: &str = "Drop selected identity from database"; +const MARGIN: i32 = 8; + +const HEADING: &str = "Delete"; +const BODY: &str = "Permanently delete selected identity from database?"; +const RESPONSE_CANCEL: (&str, &str) = ("cancel", "Cancel"); +const RESPONSE_CONFIRM: (&str, &str) = ("confirm", "Confirm"); + +pub struct Drop { + profile_identity_gemini_id: Rc>>, + pub gobject: Button, +} + +impl Drop { + // Constructors + + /// Create new `Self` + pub fn new(profile: Rc, action: Rc) -> Self { + // Init selected option holder + let profile_identity_gemini_id = Rc::new(RefCell::new(None::)); + + // Init `GObject` + let gobject = Button::builder() + .label(LABEL) + .margin_top(MARGIN) + .tooltip_text(TOOLTIP_TEXT) + .visible(false) + .build(); + + // Init events + gobject.connect_clicked({ + let action = action.clone(); + let gobject = gobject.clone(); + let profile_identity_gemini_id = profile_identity_gemini_id.clone(); + move |_| { + // Get selected identity from holder + match profile_identity_gemini_id.borrow().as_ref() { + Some(profile_identity_gemini_id) => { + // Init main `GObject` + let dialog = AlertDialog::builder() + .heading(HEADING) + .body(BODY) + .close_response(RESPONSE_CANCEL.0) + .default_response(RESPONSE_CANCEL.0) + .build(); + + // Set response variants + dialog.add_responses(&[RESPONSE_CANCEL, RESPONSE_CONFIRM]); + + // Decorate default response preset + dialog.set_response_appearance( + RESPONSE_CONFIRM.0, + ResponseAppearance::Suggested, + ); + + dialog.set_response_appearance( + RESPONSE_CANCEL.0, + ResponseAppearance::Destructive, + ); + + // Connect confirmation event + dialog.connect_response(Some(RESPONSE_CONFIRM.0), { + let action = action.clone(); + let profile = profile.clone(); + let gobject = gobject.clone(); + let profile_identity_gemini_id = profile_identity_gemini_id.clone(); + move |_, _| { + match profile.identity.gemini.delete(profile_identity_gemini_id) { + Ok(_) => { + gobject.set_css_classes(&["success"]); + gobject.set_label("Identity successfully deleted") + } + Err(e) => { + gobject.set_css_classes(&["error"]); + gobject.set_label(&e.to_string()) + } + } + action.update.activate() + } + }); + + // Show dialog + dialog.present(Some(&gobject)) + } + None => todo!(), // unexpected + } + } + }); + + // Return activated `Self` + Self { + profile_identity_gemini_id, + gobject, + } + } + + // Actions + + /// Update `profile_identity_gemini_id` holder, + /// toggle visibility depending on given value + pub fn update(&self, profile_identity_gemini_id: Option) { + self.gobject.set_visible(match profile_identity_gemini_id { + Some(value) => { + self.profile_identity_gemini_id.replace(Some(value)); + true + } + None => { + self.profile_identity_gemini_id.replace(None); + false + } + }) + } +} diff --git a/src/profile/identity/gemini.rs b/src/profile/identity/gemini.rs index 3647f68e..2285144d 100644 --- a/src/profile/identity/gemini.rs +++ b/src/profile/identity/gemini.rs @@ -68,6 +68,20 @@ impl Gemini { } } + /// Delete record from database including children dependencies, update memory index + pub fn delete(&self, profile_identity_gemini_id: i64) -> Result<(), Error> { + match self.auth.remove_ref(profile_identity_gemini_id) { + Ok(_) => match self.database.delete(profile_identity_gemini_id) { + Ok(_) => { + self.index()?; + Ok(()) + } + Err(reason) => Err(Error::Database(reason)), + }, + Err(e) => Err(Error::Auth(e)), + } + } + /// Generate new certificate and insert record to DB, update memory index /// * return new `profile_identity_gemini_id` on success pub fn make(&self, time: Option<(DateTime, DateTime)>, name: &str) -> Result { diff --git a/src/profile/identity/gemini/auth.rs b/src/profile/identity/gemini/auth.rs index ec91323b..07b8ad89 100644 --- a/src/profile/identity/gemini/auth.rs +++ b/src/profile/identity/gemini/auth.rs @@ -43,7 +43,7 @@ impl Auth { /// * return last insert `profile_identity_gemini_auth_id` on success pub fn apply(&self, profile_identity_gemini_id: i64, scope: &str) -> Result { // Cleanup records match `scope` (unauthorize) - self.remove(scope)?; + self.remove_scope(scope)?; // Create new record (auth) let profile_identity_gemini_auth_id = @@ -60,8 +60,24 @@ impl Auth { } /// Remove all records match request (unauthorize) - pub fn remove(&self, scope: &str) -> Result<(), Error> { - match self.database.records(Some(scope)) { + pub fn remove_scope(&self, scope: &str) -> Result<(), Error> { + match self.database.records_scope(Some(scope)) { + Ok(records) => { + for record in records { + if let Err(reason) = self.database.delete(record.id) { + return Err(Error::Database(reason)); + } + } + } + Err(reason) => return Err(Error::Database(reason)), + } + self.index()?; + Ok(()) + } + + /// Remove all records match `profile_identity_gemini_id` foreign reference key + pub fn remove_ref(&self, profile_identity_gemini_id: i64) -> Result<(), Error> { + match self.database.records_ref(profile_identity_gemini_id) { Ok(records) => { for record in records { if let Err(reason) = self.database.delete(record.id) { @@ -83,7 +99,7 @@ impl Auth { } // Build new index - match self.database.records(None) { + match self.database.records_scope(None) { Ok(records) => { for record in records { if let Err(reason) = self diff --git a/src/profile/identity/gemini/auth/database.rs b/src/profile/identity/gemini/auth/database.rs index c31e5d5d..aa634eb0 100644 --- a/src/profile/identity/gemini/auth/database.rs +++ b/src/profile/identity/gemini/auth/database.rs @@ -60,10 +60,17 @@ impl Database { // Getters /// Get records from database match current `profile_id` optionally filtered by `scope` - pub fn records(&self, scope: Option<&str>) -> Result, Error> { + pub fn records_scope(&self, scope: Option<&str>) -> Result, Error> { let readable = self.connection.read().unwrap(); // @TODO let tx = readable.unchecked_transaction()?; - select(&tx, scope) + select_scope(&tx, scope) + } + + /// Get records from database match current `profile_id` optionally filtered by `scope` + pub fn records_ref(&self, profile_identity_gemini_id: i64) -> Result, Error> { + let readable = self.connection.read().unwrap(); // @TODO + let tx = readable.unchecked_transaction()?; + select_ref(&tx, profile_identity_gemini_id) } } @@ -105,7 +112,7 @@ pub fn delete(tx: &Transaction, id: i64) -> Result { ) } -pub fn select(tx: &Transaction, scope: Option<&str>) -> Result, Error> { +pub fn select_scope(tx: &Transaction, scope: Option<&str>) -> Result, Error> { let mut stmt = tx.prepare( "SELECT `id`, `profile_identity_gemini_id`, @@ -133,6 +140,34 @@ pub fn select(tx: &Transaction, scope: Option<&str>) -> Result, Error Ok(records) } +pub fn select_ref(tx: &Transaction, profile_identity_gemini_id: i64) -> Result, Error> { + let mut stmt = tx.prepare( + "SELECT `id`, + `profile_identity_gemini_id`, + `scope` + + FROM `profile_identity_gemini_auth` + WHERE `profile_identity_gemini_id` = ?", + )?; + + let result = stmt.query_map([profile_identity_gemini_id], |row| { + Ok(Table { + id: row.get(0)?, + profile_identity_gemini_id: row.get(1)?, + scope: row.get(2)?, + }) + })?; + + let mut records = Vec::new(); + + for record in result { + let table = record?; + records.push(table); + } + + Ok(records) +} + pub fn last_insert_id(tx: &Transaction) -> i64 { tx.last_insert_rowid() } diff --git a/src/profile/identity/gemini/database.rs b/src/profile/identity/gemini/database.rs index 73d7e553..7e06be12 100644 --- a/src/profile/identity/gemini/database.rs +++ b/src/profile/identity/gemini/database.rs @@ -45,6 +45,22 @@ impl Database { } } + /// Delete record with given `id` from database + pub fn delete(&self, id: i64) -> Result<(), Error> { + // Begin new transaction + let mut writable = self.connection.write().unwrap(); // @TODO + let tx = writable.transaction()?; + + // Create new record + delete(&tx, id)?; + + // Done + match tx.commit() { + Ok(_) => Ok(()), + Err(reason) => Err(reason), + } + } + /// Get single record match `id` pub fn record(&self, id: i64) -> Result, Error> { let readable = self.connection.read().unwrap(); @@ -94,6 +110,10 @@ pub fn insert(tx: &Transaction, profile_identity_id: i64, pem: &str) -> Result Result { + tx.execute("DELETE FROM `profile_identity_gemini` WHERE `id` = ?", [id]) +} + pub fn select(tx: &Transaction, profile_identity_id: i64) -> Result, Error> { let mut stmt = tx.prepare( "SELECT `id`, diff --git a/src/profile/identity/gemini/error.rs b/src/profile/identity/gemini/error.rs index ef07b6d8..8097a694 100644 --- a/src/profile/identity/gemini/error.rs +++ b/src/profile/identity/gemini/error.rs @@ -11,14 +11,14 @@ pub enum Error { impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Auth(reason) => write!(f, "Could not create auth: {reason}"), - Self::Certificate(reason) => { - write!(f, "Could not create certificate: {reason}") + Self::Auth(e) => write!(f, "Could not create auth: {e}"), + Self::Certificate(e) => { + write!(f, "Could not create certificate: {e}") } - Self::Database(reason) => { - write!(f, "Database error: {reason}") + Self::Database(e) => { + write!(f, "Database error: {e}") } - Self::Memory(reason) => write!(f, "Memory error: {reason}"), + Self::Memory(e) => write!(f, "Memory error: {e}"), } } }