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
pub fn new(profile: Rc<Profile>, action: Rc<Action>, auth_uri: Uri) -> Self {
// Init widget
let widget = Rc::new(Widget::new());
let widget = Rc::new(Widget::new(profile.clone()));
// Init shared components
let auth_url = auth_uri.to_string();

View File

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

View File

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

View File

@ -101,7 +101,7 @@ impl File {
/// Change visibility status
/// * 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);
if is_visible {
self.gobject.grab_focus();

View File

@ -39,7 +39,7 @@ impl Name {
/// Change visibility status
/// * 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);
if is_visible {
self.gobject.grab_focus();

View File

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