diff --git a/src/app/browser/window/tab/item/identity/gemini.rs b/src/app/browser/window/tab/item/identity/gemini.rs index 7aaa3f2f..80988611 100644 --- a/src/app/browser/window/tab/item/identity/gemini.rs +++ b/src/app/browser/window/tab/item/identity/gemini.rs @@ -3,15 +3,9 @@ use widget::{form::list::item::value::Value, Widget}; use crate::app::browser::window::Action; use crate::profile::Profile; -use gtk::{ - gio::{prelude::TlsCertificateExt, TlsCertificate}, - glib::{gformat, Uri}, - prelude::IsA, -}; +use gtk::{glib::Uri, prelude::IsA}; use std::rc::Rc; -const DATE_FORMAT: &str = "%Y.%m.%d"; - pub struct Gemini { // profile: Rc, widget: Rc, @@ -20,136 +14,13 @@ pub struct Gemini { impl Gemini { // Construct - /// Create new `Self` for given Profile + /// Create new `Self` for given `Profile` pub fn new(profile: Rc, action: Rc, auth_uri: Uri) -> Self { - // Init widget - let widget = Rc::new(Widget::new(profile.clone())); - // Init shared URL string from URI - let url = auth_uri.to_string(); + let auth_url = auth_uri.to_string(); - // Add guest option - widget.form.list.append( - Value::UseGuestSession, - "Guest session", - "No identity for this request", - None, - false, - ); - - // Add new identity option - widget.form.list.append( - Value::GenerateNewAuth, - "Create new", - "Generate long-term certificate", - None, - false, - ); - - // Add import existing identity option - widget.form.list.append( - Value::ImportPem, - "Import identity", - "Use existing certificate", - None, - false, - ); - - // Collect identities as options from profile database - // * memory cache synced also and could be faster @TODO - match profile.identity.gemini.database.records() { - Ok(identities) => { - for identity in identities { - // Get certificate details - let certificate = match TlsCertificate::from_pem(&identity.pem) { - Ok(certificate) => certificate, - Err(reason) => todo!("{reason}"), - }; - - // Init tooltip components - let mut tooltip = "Certificate\n".to_string(); - - if let Some(subject_name) = certificate.subject_name() { - tooltip - .push_str(&format!("\nsubject\n{subject_name}")); - } - - if let Some(issuer_name) = certificate.issuer_name() { - tooltip.push_str(&format!("\nissuer\n{issuer_name}")); - } - - if let Some(not_valid_before) = certificate.not_valid_before() { - if let Ok(timestamp) = not_valid_before.format_iso8601() { - tooltip.push_str(&format!( - "\nvalid after\n{timestamp}" - )); - } - } - - if let Some(not_valid_after) = certificate.not_valid_after() { - if let Ok(timestamp) = not_valid_after.format_iso8601() { - tooltip.push_str(&format!( - "\nvalid before\n{timestamp}" - )); - } - } - - // Collect scope info - let mut scope = Vec::new(); - - for auth in profile - .identity - .gemini - .auth - .database - .records_scope(None) - .unwrap() - .iter() - .filter(|this| this.profile_identity_gemini_id == identity.id) - { - scope.push(format!("{}", auth.scope.clone())) - } - - if !scope.is_empty() { - tooltip.push_str(&format!("\n\nScope\n\n{}", scope.join("\n"))); - } - - // Append record option - widget.form.list.append( - Value::ProfileIdentityGeminiId(identity.id), - // title - &certificate - .subject_name() - .unwrap_or(gformat!("Unknown")) - .replace("CN=", ""), // trim prefix - // subtitle - &format!( - "{} - {} | scope: {}", - certificate - .not_valid_before() - .unwrap() // @TODO - .format(DATE_FORMAT) - .unwrap(), - certificate - .not_valid_after() - .unwrap() // @TODO - .format(DATE_FORMAT) - .unwrap(), - scope.len(), - ), - Some(&tooltip), - profile - .identity - .gemini - .auth - .memory - .match_scope(&url) - .is_some_and(|auth| auth.profile_identity_gemini_id == identity.id), // is selected - ); - } - } - Err(e) => todo!("{e}"), - } // @TODO separate markup + // Init widget + let widget = Rc::new(Widget::new(profile.clone(), &auth_url)); // Init events widget.on_apply({ @@ -158,8 +29,8 @@ impl Gemini { // Get option match user choice let option = match response { Value::ProfileIdentityGeminiId(value) => Some(value), - Value::UseGuestSession => None, - Value::GenerateNewAuth => Some( + Value::GuestSession => None, + Value::GeneratePem => Some( match profile .identity .gemini @@ -189,14 +60,14 @@ impl Gemini { .identity .gemini .auth - .apply(profile_identity_gemini_id, &url) + .apply(profile_identity_gemini_id, &auth_url) { todo!("{}", reason.to_string()) }; } // Remove all identity auths for `auth_uri` None => { - if let Err(reason) = profile.identity.gemini.auth.remove_scope(&url) { + if let Err(reason) = profile.identity.gemini.auth.remove_scope(&auth_url) { todo!("{}", reason.to_string()) }; } diff --git a/src/app/browser/window/tab/item/identity/gemini/widget.rs b/src/app/browser/window/tab/item/identity/gemini/widget.rs index a1e4f300..5758e600 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget.rs @@ -33,12 +33,12 @@ impl Widget { // Constructors /// Create new `Self` - pub fn new(profile: Rc) -> Self { + pub fn new(profile: Rc, auth_url: &str) -> Self { // Init actions let action = Rc::new(Action::new()); // Init child container - let form = Rc::new(Form::new(profile, action.clone())); + let form = Rc::new(Form::new(profile, action.clone(), auth_url)); // Init main widget let alert_dialog = AlertDialog::builder() 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 e6de1b2c..15e73c3a 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 @@ -32,10 +32,10 @@ impl Form { // Constructors /// Create new `Self` - pub fn new(profile: Rc, action: Rc) -> Self { + pub fn new(profile: Rc, action: Rc, auth_url: &str) -> Self { // Init components let file = Rc::new(File::new(action.clone())); - let list = Rc::new(List::new()); + let list = Rc::new(List::new(profile.clone(), auth_url)); let name = Rc::new(Name::new(action.clone())); let save = Rc::new(Save::new(profile.clone())); let drop = Rc::new(Drop::new(profile.clone(), action.clone(), list.clone())); @@ -61,7 +61,7 @@ impl Form { let update = action.update.clone(); move |item| { // Change name entry visibility - name.update(matches!(item, Value::GenerateNewAuth)); + name.update(matches!(item, Value::GeneratePem)); // Change file choose button visibility file.update(matches!(item, Value::ImportPem)); @@ -103,7 +103,7 @@ impl Form { /// Validate `Self` components match current selection pub fn is_applicable(&self) -> bool { match self.list.selected_item().value_enum() { - Value::GenerateNewAuth => self.name.is_valid(), + Value::GeneratePem => self.name.is_valid(), Value::ImportPem => self.file.is_valid(), Value::ProfileIdentityGeminiId(_) => !self.list.selected_item().is_active(), _ => true, diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs index dea339ef..3d449814 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list.rs @@ -1,6 +1,9 @@ pub mod item; +use std::rc::Rc; + use item::{value::Value, Item}; +use crate::profile::Profile; use gtk::{ gdk::Cursor, gio::{ @@ -20,10 +23,33 @@ impl List { // Constructors /// Create new `Self` - pub fn new() -> Self { - // Init `ListStore` with custom `DropDown` properties + pub fn new(profile: Rc, auth_url: &str) -> Self { + // Init model let list_store = ListStore::new::(); + list_store.append(&Item::new_guest_session()); + list_store.append(&Item::new_generate_pem()); + list_store.append(&Item::new_import_pem()); + + // Append identities from profile database + // * memory cache synced also and could be faster @TODO + match profile.identity.gemini.database.records() { + Ok(identities) => { + for identity in identities { + match Item::new_profile_identity_gemini_id( + profile.clone(), + identity.id, + &identity.pem, + auth_url, + ) { + Ok(item) => list_store.append(&item), + Err(_) => todo!(), + } + } + } + Err(_) => todo!(), + } + // Setup item factory // * wanted only to append items after `DropDown` init let factory = SignalListItemFactory::new(); @@ -99,6 +125,13 @@ impl List { .factory(&factory) .build(); + // Select active record + dropdown.set_selected( + list_store + .find_with_equal_func(|item| item.dynamic_cast_ref::().unwrap().is_active()) + .unwrap(), + ); // @TODO panic or handle? + // Return activated `Self` Self { list_store, @@ -108,25 +141,6 @@ impl List { // Actions - /// Append new item - pub fn append( - &self, - value: Value, - title: &str, - subtitle: &str, - tooltip: Option<&str>, - is_active: bool, - ) { - let item = Item::new(value, title, subtitle, tooltip, is_active); - - self.list_store.append(&item); - - if is_active { - self.dropdown - .set_selected(self.list_store.find(&item).unwrap()); // @TODO panic or handle? - } - } - /// Find list item by `value` (stores ID) /// * return `position` found pub fn find(&self, value: i64) -> Option { diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item.rs index eff046e0..47fdcc14 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item.rs @@ -1,8 +1,21 @@ +mod error; mod imp; +mod is_active; +mod subtitle; +mod title; +mod tooltip; pub mod value; -use gtk::glib::{self, Object}; -use value::Value; +use std::rc::Rc; + +pub use error::Error; +pub use value::Value; + +use crate::profile::Profile; +use gtk::{ + gio::TlsCertificate, + glib::{self, Object}, +}; glib::wrapper! { pub struct Item(ObjectSubclass); @@ -10,47 +23,105 @@ glib::wrapper! { // C-type property `value` conversion for `Item` // * values > 0 reserved for `profile_identity_gemini_id` -const G_VALUE_GENERATE_NEW_AUTH: i64 = 0; +const G_VALUE_GENERATE_PEM: i64 = 0; const G_VALUE_IMPORT_PEM: i64 = -1; -const G_VALUE_USE_GUEST_SESSION: i64 = -2; +const G_VALUE_GUEST_SESSION: i64 = -2; impl Item { // Constructors - /// Create new `GObject` - pub fn new( - value: Value, - title: &str, - subtitle: &str, - tooltip: Option<&str>, - is_active: bool, - ) -> Self { + pub fn new_guest_session() -> Self { Object::builder() - .property( - "value", - match value { - Value::GenerateNewAuth => G_VALUE_GENERATE_NEW_AUTH, - Value::ImportPem => G_VALUE_IMPORT_PEM, - Value::UseGuestSession => G_VALUE_USE_GUEST_SESSION, - Value::ProfileIdentityGeminiId(value) => value, - }, - ) - .property("title", title) - .property("subtitle", subtitle) - .property("tooltip", tooltip.unwrap_or_default()) - .property("is_active", is_active) + .property("value", G_VALUE_GUEST_SESSION) + .property("title", "Guest session") + .property("subtitle", "No identity for this request") .build() } + pub fn new_generate_pem() -> Self { + Object::builder() + .property("value", G_VALUE_GENERATE_PEM) + .property("title", "Create new") + .property("subtitle", "Generate long-term certificate") + .build() + } + + pub fn new_import_pem() -> Self { + Object::builder() + .property("value", G_VALUE_IMPORT_PEM) + .property("title", "Import identity") + .property("subtitle", "Use existing certificate") + .build() + } + + pub fn new_profile_identity_gemini_id( + profile: Rc, + profile_identity_gemini_id: i64, + pem: &str, + auth_url: &str, + ) -> Result { + match TlsCertificate::from_pem(pem) { + Ok(certificate) => { + // Collect shared certificate scope + let scope = scope(profile.clone(), profile_identity_gemini_id); + + // Build GObject + Ok(Object::builder() + .property("value", profile_identity_gemini_id) + .property( + "title", + title::new_for_profile_identity_gemini_id(&certificate), + ) + .property( + "subtitle", + subtitle::new_for_profile_identity_gemini_id(&certificate, &scope), + ) + .property( + "tooltip", + tooltip::new_for_profile_identity_gemini_id(&certificate, &scope), + ) + .property( + "is_active", + is_active::new_for_profile_identity_gemini_id( + profile, + profile_identity_gemini_id, + auth_url, + ), + ) + .build()) + } + Err(e) => Err(Error::TlsCertificate(e)), + } + } + // Getters - /// Get `value` as enum `Value` + /// Get `Self` C-value as `Value` pub fn value_enum(&self) -> Value { match self.value() { - G_VALUE_GENERATE_NEW_AUTH => Value::GenerateNewAuth, + G_VALUE_GENERATE_PEM => Value::GeneratePem, + G_VALUE_GUEST_SESSION => Value::GuestSession, G_VALUE_IMPORT_PEM => Value::ImportPem, - G_VALUE_USE_GUEST_SESSION => Value::UseGuestSession, value => Value::ProfileIdentityGeminiId(value), } } } + +// Tools + +fn scope(profile: Rc, profile_identity_gemini_id: i64) -> Vec { + let mut scope = Vec::new(); + for auth in profile + .identity + .gemini + .auth + .database + .records_scope(None) + .unwrap() + .iter() + .filter(|this| this.profile_identity_gemini_id == profile_identity_gemini_id) + { + scope.push(auth.scope.clone()) + } + scope +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/error.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/error.rs new file mode 100644 index 00000000..cd2f0941 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/error.rs @@ -0,0 +1,16 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + TlsCertificate(gtk::glib::Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::TlsCertificate(e) => { + write!(f, "TLS certificate error `{e}`") + } + } + } +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/is_active.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/is_active.rs new file mode 100644 index 00000000..d30c5c0f --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/is_active.rs @@ -0,0 +1,16 @@ +use crate::profile::Profile; +use std::rc::Rc; + +pub fn new_for_profile_identity_gemini_id( + profile: Rc, + profile_identity_gemini_id: i64, + auth_url: &str, +) -> bool { + profile + .identity + .gemini + .auth + .memory + .match_scope(&auth_url) + .is_some_and(|auth| auth.profile_identity_gemini_id == profile_identity_gemini_id) +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/subtitle.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/subtitle.rs new file mode 100644 index 00000000..8fb6b741 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/subtitle.rs @@ -0,0 +1,23 @@ +use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt}; + +const DATE_FORMAT: &str = "%Y.%m.%d"; + +pub fn new_for_profile_identity_gemini_id( + certificate: &TlsCertificate, + scope: &[String], +) -> String { + format!( + "{} - {} | scope: {}", + certificate + .not_valid_before() + .unwrap() // @TODO + .format(DATE_FORMAT) + .unwrap(), + certificate + .not_valid_after() + .unwrap() // @TODO + .format(DATE_FORMAT) + .unwrap(), + scope.len(), + ) +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/title.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/title.rs new file mode 100644 index 00000000..3ad0d8e8 --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/title.rs @@ -0,0 +1,8 @@ +use gtk::{gio::TlsCertificate, glib::gformat, prelude::TlsCertificateExt}; + +pub fn new_for_profile_identity_gemini_id(certificate: &TlsCertificate) -> String { + certificate + .subject_name() + .unwrap_or(gformat!("Unknown")) + .replace("CN=", "") +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/tooltip.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/tooltip.rs new file mode 100644 index 00000000..3bb4878e --- /dev/null +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/tooltip.rs @@ -0,0 +1,40 @@ +use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt}; + +pub fn new_for_profile_identity_gemini_id( + certificate: &TlsCertificate, + scope: &Vec, +) -> String { + let mut tooltip = "Certificate\n".to_string(); + + if let Some(subject_name) = certificate.subject_name() { + tooltip.push_str(&format!("\nsubject\n{subject_name}")); + } + + if let Some(issuer_name) = certificate.issuer_name() { + tooltip.push_str(&format!("\nissuer\n{issuer_name}")); + } + + if let Some(not_valid_before) = certificate.not_valid_before() { + if let Ok(timestamp) = not_valid_before.format_iso8601() { + tooltip.push_str(&format!("\nvalid after\n{timestamp}")); + } + } + + if let Some(not_valid_after) = certificate.not_valid_after() { + if let Ok(timestamp) = not_valid_after.format_iso8601() { + tooltip.push_str(&format!( + "\nvalid before\n{timestamp}" + )); + } + } + + if !scope.is_empty() { + tooltip.push_str("\n\nScope\n"); + + for path in scope { + tooltip.push_str(&format!("\n{}", path)); + } + } + + tooltip +} diff --git a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/value.rs b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/value.rs index c93c210d..9740d638 100644 --- a/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/value.rs +++ b/src/app/browser/window/tab/item/identity/gemini/widget/form/list/item/value.rs @@ -1,7 +1,7 @@ #[derive(Debug)] pub enum Value { - GenerateNewAuth, + GeneratePem, + GuestSession, ImportPem, ProfileIdentityGeminiId(i64), - UseGuestSession, }