complete certificate export feature

This commit is contained in:
yggverse 2024-11-29 02:36:26 +02:00
parent 8a62ef46df
commit 3dfcbc279d
9 changed files with 182 additions and 81 deletions

View File

@ -23,7 +23,7 @@ impl Gemini {
/// Create new `Self` for given Profile /// Create new `Self` for given Profile
pub fn new(profile: Rc<Profile>, action: Rc<Action>, auth_uri: Uri) -> Self { pub fn new(profile: Rc<Profile>, action: Rc<Action>, auth_uri: Uri) -> Self {
// Init widget // Init widget
let widget = Rc::new(Widget::new()); let widget = Rc::new(Widget::new(profile.clone()));
// Init shared components // Init shared components
let auth_url = auth_uri.to_string(); let auth_url = auth_uri.to_string();

View File

@ -4,6 +4,7 @@ pub mod form;
use action::Action; use action::Action;
use form::{list::item::value::Value, Form}; use form::{list::item::value::Value, Form};
use crate::profile::Profile;
use adw::{ use adw::{
prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual}, prelude::{AdwDialogExt, AlertDialogExt, AlertDialogExtManual},
AlertDialog, ResponseAppearance, AlertDialog, ResponseAppearance,
@ -32,12 +33,12 @@ impl Widget {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new() -> Self { pub fn new(profile: Rc<Profile>) -> Self {
// Init actions // Init actions
let action = Rc::new(Action::new()); let action = Rc::new(Action::new());
// Init child container // Init child container
let form = Rc::new(Form::new(action.clone())); let form = Rc::new(Form::new(profile, action.clone()));
// Init main `GObject` // Init main `GObject`
let gobject = AlertDialog::builder() let gobject = AlertDialog::builder()

View File

@ -9,6 +9,7 @@ use name::Name;
use save::Save; use save::Save;
use super::Action; use super::Action;
use crate::profile::Profile;
use gtk::{prelude::BoxExt, Box, Orientation}; use gtk::{prelude::BoxExt, Box, Orientation};
use std::rc::Rc; use std::rc::Rc;
@ -17,7 +18,7 @@ pub struct Form {
pub file: Rc<File>, pub file: Rc<File>,
pub list: Rc<List>, pub list: Rc<List>,
pub name: Rc<Name>, pub name: Rc<Name>,
pub save: Rc<Save>, // pub save: Rc<Save>,
pub gobject: Box, pub gobject: Box,
} }
@ -25,12 +26,12 @@ impl Form {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(action: Rc<Action>) -> Self { pub fn new(profile: Rc<Profile>, action: Rc<Action>) -> Self {
// Init components // Init components
let file = Rc::new(File::new(action.clone())); let file = Rc::new(File::new(action.clone()));
let list = Rc::new(List::new()); let list = Rc::new(List::new());
let name = Rc::new(Name::new(action.clone())); let name = Rc::new(Name::new(action.clone()));
let save = Rc::new(Save::new(action.clone())); let save = Rc::new(Save::new(profile));
// Init main container // Init main container
let gobject = Box::builder().orientation(Orientation::Vertical).build(); let gobject = Box::builder().orientation(Orientation::Vertical).build();
@ -48,18 +49,18 @@ impl Form {
let update = action.update.clone(); let update = action.update.clone();
move |item| { move |item| {
// Change name entry visibility // Change name entry visibility
name.show(matches!(item, Value::GenerateNewAuth)); name.update(matches!(item, Value::GenerateNewAuth));
// Change file choose button visibility // Change file choose button visibility
file.show(matches!(item, Value::ImportPem)); file.update(matches!(item, Value::ImportPem));
// Change export button visibility, update holder @TODO // Change export button visibility by update it holder value
match item { match item {
Value::ProfileIdentityGeminiId(id) => { Value::ProfileIdentityGeminiId(value) => {
// save.show(Some(id)); save.update(Some(value));
} }
_ => { _ => {
// save.show(None); save.update(None);
} }
} }
@ -75,7 +76,7 @@ impl Form {
file, file,
list, list,
name, name,
save, // save,
} }
} }

View File

@ -101,7 +101,7 @@ impl File {
/// Change visibility status /// Change visibility status
/// * grab focus on `is_visible` /// * grab focus on `is_visible`
pub fn show(&self, is_visible: bool) { pub fn update(&self, is_visible: bool) {
self.gobject.set_visible(is_visible); self.gobject.set_visible(is_visible);
if is_visible { if is_visible {
self.gobject.grab_focus(); self.gobject.grab_focus();

View File

@ -39,7 +39,7 @@ impl Name {
/// Change visibility status /// Change visibility status
/// * grab focus on `is_visible` /// * grab focus on `is_visible`
pub fn show(&self, is_visible: bool) { pub fn update(&self, is_visible: bool) {
self.gobject.set_visible(is_visible); self.gobject.set_visible(is_visible);
if is_visible { if is_visible {
self.gobject.grab_focus(); self.gobject.grab_focus();

View File

@ -1,7 +1,7 @@
mod certificate; mod certificate;
use certificate::Certificate; use certificate::Certificate;
use super::Action; use crate::profile::Profile;
use gtk::{ use gtk::{
gio::{Cancellable, ListStore}, gio::{Cancellable, ListStore},
prelude::{ButtonExt, FileExt, WidgetExt}, prelude::{ButtonExt, FileExt, WidgetExt},
@ -13,7 +13,7 @@ const LABEL: &str = "Export to file..";
const MARGIN: i32 = 8; const MARGIN: i32 = 8;
pub struct Save { pub struct Save {
certificate: Rc<RefCell<Option<Certificate>>>, profile_identity_gemini_id: Rc<RefCell<Option<i64>>>,
pub gobject: Button, pub gobject: Button,
} }
@ -21,9 +21,9 @@ impl Save {
// Constructors // Constructors
/// Create new `Self` /// Create new `Self`
pub fn new(action: Rc<Action>) -> Self { pub fn new(profile: Rc<Profile>) -> Self {
// Init `Certificate` holder // Init selected option holder
let certificate = Rc::new(RefCell::new(None::<Certificate>)); let profile_identity_gemini_id = Rc::new(RefCell::new(None));
// Init `GObject` // Init `GObject`
let gobject = Button::builder() let gobject = Button::builder()
@ -34,75 +34,91 @@ impl Save {
// Init events // Init events
gobject.connect_clicked({ gobject.connect_clicked({
let certificate = certificate.clone(); let profile_identity_gemini_id = profile_identity_gemini_id.clone();
let gobject = gobject.clone(); let gobject = gobject.clone();
move |_| { move |_| {
// Get certificate selected from holder // Get selected identity from holder
match certificate.borrow().as_ref() { match profile_identity_gemini_id.borrow().as_ref() {
Some(certificate) => { Some(profile_identity_gemini_id) => {
// Copy certificate holder values
let name = certificate.name.clone();
let data = certificate.data.clone();
// Lock open button (prevent double click) // Lock open button (prevent double click)
gobject.set_sensitive(false); gobject.set_sensitive(false);
// Init file filters related with PEM extension // Create PEM file based on option ID selected
let filters = ListStore::new::<FileFilter>(); match Certificate::new(profile.clone(), *profile_identity_gemini_id) {
Ok(certificate) => {
// Init file filters related with PEM extension
let filters = ListStore::new::<FileFilter>();
let filter_all = FileFilter::new(); let filter_all = FileFilter::new();
filter_all.add_pattern("*.*"); filter_all.add_pattern("*.*");
filter_all.set_name(Some("All")); filter_all.set_name(Some("All"));
filters.append(&filter_all); filters.append(&filter_all);
let filter_pem = FileFilter::new(); let filter_pem = FileFilter::new();
filter_pem.add_mime_type("application/x-x509-ca-cert"); filter_pem.add_mime_type("application/x-x509-ca-cert");
filter_pem.set_name(Some("Certificate (*.pem)")); filter_pem.set_name(Some("Certificate (*.pem)"));
filters.append(&filter_pem); filters.append(&filter_pem);
// Init file dialog // Init file dialog
FileDialog::builder() FileDialog::builder()
.default_filter(&filter_pem) .default_filter(&filter_pem)
.filters(&filters) .filters(&filters)
.initial_name(format!("{name}.pem")) .initial_name(format!("{}.pem", certificate.name))
.build() .build()
.save(None::<&Window>, None::<&Cancellable>, { .save(None::<&Window>, None::<&Cancellable>, {
let gobject = gobject.clone(); let gobject = gobject.clone();
move |result| { move |result| {
match result { match result {
Ok(file) => match file.path() { Ok(file) => match file.path() {
Some(path) => match File::create(&path) { Some(path) => match File::create(&path) {
Ok(mut destination) => { Ok(mut destination) => {
match destination.write_all(data.as_bytes()) { match destination.write_all(
Ok(_) => { certificate.data.as_bytes(),
// @TODO ) {
gobject.set_css_classes(&["success"]); Ok(_) => {
gobject.set_label(&format!( gobject.set_css_classes(&[
"Saved to {}", "success",
path.to_string_lossy() ]);
)) gobject.set_label(&format!(
"Saved to {}",
path.to_string_lossy()
))
}
Err(e) => {
gobject.set_css_classes(&[
"error",
]);
gobject
.set_label(&e.to_string())
}
}
} }
Err(reason) => { Err(e) => {
gobject.set_css_classes(&["error"]); gobject.set_css_classes(&["error"]);
gobject.set_label(&reason.to_string()) gobject.set_label(&e.to_string())
} }
},
None => {
gobject.set_css_classes(&["warning"]);
gobject.set_label(
"Could not init destination path",
)
} }
},
Err(e) => {
gobject.set_css_classes(&["warning"]);
gobject.set_label(e.message())
} }
Err(reason) => { }
gobject.set_css_classes(&["error"]); gobject.set_sensitive(true); // unlock
gobject.set_label(&reason.to_string())
}
},
None => todo!(),
},
Err(reason) => {
gobject.set_css_classes(&["warning"]);
gobject.set_label(reason.message())
} }
} });
gobject.set_sensitive(true); // unlock }
} Err(e) => {
}); gobject.set_css_classes(&["error"]);
gobject.set_label(&e.to_string())
}
}
} }
None => todo!(), // unexpected None => todo!(), // unexpected
} }
@ -111,16 +127,25 @@ impl Save {
// Return activated `Self` // Return activated `Self`
Self { Self {
certificate, profile_identity_gemini_id,
gobject, gobject,
} }
} }
// Actions // Actions
/// Change visibility status /// Update `profile_identity_gemini_id` holder,
/// * grab focus on `is_visible` /// toggle visibility depending on given value
pub fn show(&self, is_visible: bool) { pub fn update(&self, profile_identity_gemini_id: Option<i64>) {
self.gobject.set_visible(is_visible) 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
}
})
} }
} }

View File

@ -1,4 +1,38 @@
mod error;
pub use error::Error;
use crate::profile::Profile;
use gtk::{gio::TlsCertificate, prelude::TlsCertificateExt};
use std::rc::Rc;
/// Certificate details holder for export to file action
pub struct Certificate { pub struct Certificate {
pub name: String,
pub data: String, pub data: String,
pub name: String,
}
impl Certificate {
// Constructors
/// Create new `Self`
pub fn new(profile: Rc<Profile>, profile_identity_gemini_id: i64) -> Result<Self, Error> {
match profile
.identity
.gemini
.database
.record(profile_identity_gemini_id)
{
Ok(record) => match record {
Some(identity) => match TlsCertificate::from_pem(&identity.pem) {
Ok(certificate) => Ok(Self {
data: identity.pem,
name: certificate.subject_name().unwrap().replace("CN=", ""),
}),
Err(reason) => Err(Error::TlsCertificate(reason)),
},
None => Err(Error::NotFound(profile_identity_gemini_id)),
},
Err(reason) => Err(Error::Database(reason)),
}
}
} }

View File

@ -0,0 +1,25 @@
use gtk::glib;
use std::fmt::{Display, Formatter, Result};
#[derive(Debug)]
pub enum Error {
Database(sqlite::Error),
NotFound(i64),
TlsCertificate(glib::Error),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> Result {
match self {
Self::Database(e) => {
write!(f, "Database error: {e}")
}
Self::NotFound(profile_identity_gemini_id) => {
write!(f, "Record for `{profile_identity_gemini_id}` not found")
}
Self::TlsCertificate(e) => {
write!(f, "TLS certificate error: {e}")
}
}
}
}

View File

@ -45,6 +45,21 @@ impl Database {
} }
} }
/// Get single record match `id`
pub fn record(&self, id: i64) -> Result<Option<Table>, Error> {
let readable = self.connection.read().unwrap();
let tx = readable.unchecked_transaction()?;
let records = select(&tx, *self.profile_identity_id)?; // @TODO single record query
for record in records {
if record.id == id {
return Ok(Some(record));
}
}
Ok(None)
}
/// Get all records match current `profile_identity_id` /// Get all records match current `profile_identity_id`
pub fn records(&self) -> Result<Vec<Table>, Error> { pub fn records(&self) -> Result<Vec<Table>, Error> {
let readable = self.connection.read().unwrap(); // @TODO let readable = self.connection.read().unwrap(); // @TODO