implement credential saving with cross-platform keyring

This commit is contained in:
cel 🌸 2025-03-01 06:56:17 +00:00
parent 66e12f7264
commit a938d39dc5
4 changed files with 161 additions and 18 deletions

2
Cargo.lock generated
View File

@ -2340,8 +2340,10 @@ dependencies = [
"keyring", "keyring",
"luz", "luz",
"serde", "serde",
"thiserror 2.0.11",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"toml",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",

View File

@ -16,3 +16,5 @@ keyring = { version = "3", features = ["apple-native", "windows-native", "sync-s
uuid = { version = "1.13.1", features = ["v4"] } uuid = { version = "1.13.1", features = ["v4"] }
indexmap = "2.7.1" indexmap = "2.7.1"
serde = { version = "1.0.218", features = ["derive"] } serde = { version = "1.0.218", features = ["derive"] }
thiserror = "2.0.11"
toml = "0.8"

View File

@ -4,10 +4,12 @@ use iced::{
Element, Task, Element, Task,
}; };
use jid::JID; use jid::JID;
use keyring::Entry;
use luz::{ use luz::{
presence::{Offline, Presence}, presence::{Offline, Presence},
LuzHandle, LuzHandle,
}; };
use serde::{Deserialize, Serialize};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tracing::info; use tracing::info;
@ -41,6 +43,12 @@ pub enum Action {
ClientCreated(Task<crate::Message>), ClientCreated(Task<crate::Message>),
} }
#[derive(Serialize, Deserialize)]
pub struct Creds {
pub jid: String,
pub password: String,
}
impl LoginModal { impl LoginModal {
pub fn update(&mut self, message: Message) -> Action { pub fn update(&mut self, message: Message) -> Action {
match message { match message {
@ -60,6 +68,7 @@ impl LoginModal {
info!("submitting login"); info!("submitting login");
let jid_str = self.jid.clone(); let jid_str = self.jid.clone();
let password = self.password.clone(); let password = self.password.clone();
let remember_me = self.remember_me.clone();
Action::ClientCreated( Action::ClientCreated(
Task::future(async move { Task::future(async move {
let jid: Result<JID, _> = jid_str.parse(); let jid: Result<JID, _> = jid_str.parse();
@ -70,19 +79,56 @@ impl LoginModal {
.await; .await;
match result { match result {
Ok((luz_handle, receiver)) => { Ok((luz_handle, receiver)) => {
let stream = ReceiverStream::new(receiver); let mut tasks = Vec::new();
let stream = tasks.push(Task::done(crate::Message::ClientCreated(
stream.map(|message| crate::Message::Luz(message)); Client {
vec![
Task::done(crate::Message::ClientCreated(Client {
client: luz_handle, client: luz_handle,
jid: j, jid: j,
connection_status: Presence::Offline( connection_status: Presence::Offline(
Offline::default(), Offline::default(),
), ),
})), },
Task::stream(stream), )));
] let stream = ReceiverStream::new(receiver);
let stream =
stream.map(|message| crate::Message::Luz(message));
tasks.push(Task::stream(stream));
if remember_me {
let entry = Entry::new("macaw", "macaw");
match entry {
Ok(e) => {
let creds = Creds {
jid: jid_str,
password,
};
let creds = toml::to_string(&creds);
match creds {
Ok(c) => {
let result = e.set_password(&c);
if let Err(e) = result {
tasks.push(Task::done(crate::Message::Error(
crate::Error::CredentialsSave(e.into()),
)));
}
}
Err(e) => tasks.push(Task::done(
crate::Message::Error(
crate::Error::CredentialsSave(
e.into(),
),
),
)),
}
}
Err(e) => {
tasks.push(Task::done(crate::Message::Error(
crate::Error::CredentialsSave(e.into()),
)))
}
}
}
tasks
} }
Err(_e) => { Err(_e) => {
tracing::error!("error (database probably)"); tracing::error!("error (database probably)");

View File

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::fmt::Debug; use std::fmt::Debug;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use iced::futures::{SinkExt, Stream, StreamExt}; use iced::futures::{SinkExt, Stream, StreamExt};
use iced::widget::button::Status; use iced::widget::button::Status;
@ -15,12 +16,13 @@ use iced::{stream, Color, Element, Subscription, Task, Theme};
use indexmap::{indexmap, IndexMap}; use indexmap::{indexmap, IndexMap};
use jid::JID; use jid::JID;
use keyring::Entry; use keyring::Entry;
use login_modal::LoginModal; use login_modal::{Creds, LoginModal};
use luz::chat::{Chat, Message as ChatMessage}; use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence}; use luz::presence::{Offline, Presence};
use luz::CommandMessage; use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage}; use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{mpsc, oneshot}; use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream; use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info}; use tracing::{error, info};
@ -58,11 +60,6 @@ pub struct OpenChat {
pub struct NewChat; pub struct NewChat;
pub struct Creds {
jid: String,
password: String,
}
impl Macaw { impl Macaw {
pub fn new(client: Option<Client>, config: Config) -> Self { pub fn new(client: Option<Client>, config: Config) -> Self {
let account; let account;
@ -112,11 +109,60 @@ impl Deref for Client {
} }
} }
fn main() -> iced::Result { #[tokio::main]
async fn main() -> iced::Result {
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
let cfg = confy::load("macaw", None).unwrap(); let cfg = confy::load("macaw", None).unwrap();
let client: Option<(JID, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None; let entry = Entry::new("macaw", "macaw");
let mut client_creation_error: Option<Error> = None;
let mut creds: Option<Creds> = None;
match entry {
Ok(e) => {
let result = e.get_password();
match result {
Ok(c) => {
let result = toml::from_str(&c);
match result {
Ok(c) => creds = Some(c),
Err(e) => {
client_creation_error =
Some(Error::CredentialsLoad(CredentialsLoadError::Toml(e.into())))
}
}
}
Err(e) => match e {
keyring::Error::NoEntry => {}
_ => {
client_creation_error = Some(Error::CredentialsLoad(
CredentialsLoadError::Keyring(e.into()),
))
}
},
}
}
Err(e) => {
client_creation_error = Some(Error::CredentialsLoad(CredentialsLoadError::Keyring(
e.into(),
)))
}
}
let mut client: Option<(JID, LuzHandle, mpsc::Receiver<UpdateMessage>)> = None;
if let Some(creds) = creds {
let jid = creds.jid.parse::<JID>();
match jid {
Ok(jid) => {
let luz = LuzHandle::new(jid.clone(), creds.password.to_string(), "macaw.db").await;
match luz {
Ok((handle, recv)) => client = Some((jid.as_bare(), handle, recv)),
Err(e) => client_creation_error = Some(Error::ClientCreation(e)),
}
}
Err(e) => client_creation_error = Some(Error::CredentialsLoad(e.into())),
}
}
if let Some((jid, luz_handle, update_recv)) = client { if let Some((jid, luz_handle, update_recv)) = client {
let stream = ReceiverStream::new(update_recv); let stream = ReceiverStream::new(update_recv);
@ -137,8 +183,13 @@ fn main() -> iced::Result {
) )
}) })
} else { } else {
iced::application("Macaw", Macaw::update, Macaw::view) if let Some(e) = client_creation_error {
.run_with(|| (Macaw::new(None, cfg), Task::none())) iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None, cfg), Task::done(Message::Error(e))))
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None, cfg), Task::none()))
}
} }
} }
@ -156,6 +207,47 @@ pub enum Message {
CloseChat(JID), CloseChat(JID),
MessageCompose(String), MessageCompose(String),
SendMessage(JID, String), SendMessage(JID, String),
Error(Error),
}
#[derive(Debug, Error, Clone)]
pub enum Error {
#[error("failed to create Luz client: {0}")]
ClientCreation(#[from] luz::error::DatabaseError),
#[error("failed to save credentials: {0}")]
CredentialsSave(CredentialsSaveError),
#[error("failed to load credentials: {0}")]
CredentialsLoad(CredentialsLoadError),
}
#[derive(Debug, Error, Clone)]
pub enum CredentialsSaveError {
#[error("keyring: {0}")]
Keyring(Arc<keyring::Error>),
#[error("toml serialisation: {0}")]
Toml(#[from] toml::ser::Error),
}
impl From<keyring::Error> for CredentialsSaveError {
fn from(e: keyring::Error) -> Self {
Self::Keyring(Arc::new(e))
}
}
#[derive(Debug, Error, Clone)]
pub enum CredentialsLoadError {
#[error("keyring: {0}")]
Keyring(Arc<keyring::Error>),
#[error("toml serialisation: {0}")]
Toml(#[from] toml::de::Error),
#[error("invalid jid: {0}")]
JID(#[from] jid::ParseError),
}
impl From<keyring::Error> for CredentialsLoadError {
fn from(e: keyring::Error) -> Self {
Self::Keyring(Arc::new(e))
}
} }
impl Macaw { impl Macaw {
@ -358,6 +450,7 @@ impl Macaw {
) )
.discard() .discard()
} }
Message::Error(error) => todo!("error notification toasts, logging?"),
} }
} }