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",
"luz",
"serde",
"thiserror 2.0.11",
"tokio",
"tokio-stream",
"toml",
"tracing",
"tracing-subscriber",
"uuid",

View File

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

View File

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

View File

@ -2,6 +2,7 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use iced::futures::{SinkExt, Stream, StreamExt};
use iced::widget::button::Status;
@ -15,12 +16,13 @@ use iced::{stream, Color, Element, Subscription, Task, Theme};
use indexmap::{indexmap, IndexMap};
use jid::JID;
use keyring::Entry;
use login_modal::LoginModal;
use login_modal::{Creds, LoginModal};
use luz::chat::{Chat, Message as ChatMessage};
use luz::presence::{Offline, Presence};
use luz::CommandMessage;
use luz::{roster::Contact, user::User, LuzHandle, UpdateMessage};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::sync::{mpsc, oneshot};
use tokio_stream::wrappers::ReceiverStream;
use tracing::{error, info};
@ -58,11 +60,6 @@ pub struct OpenChat {
pub struct NewChat;
pub struct Creds {
jid: String,
password: String,
}
impl Macaw {
pub fn new(client: Option<Client>, config: Config) -> Self {
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();
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 {
let stream = ReceiverStream::new(update_recv);
@ -137,8 +183,13 @@ fn main() -> iced::Result {
)
})
} else {
iced::application("Macaw", Macaw::update, Macaw::view)
.run_with(|| (Macaw::new(None, cfg), Task::none()))
if let Some(e) = client_creation_error {
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),
MessageCompose(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 {
@ -358,6 +450,7 @@ impl Macaw {
)
.discard()
}
Message::Error(error) => todo!("error notification toasts, logging?"),
}
}