From e5a68c359f7fc7833fded7f802802f91829f401f Mon Sep 17 00:00:00 2001 From: edouardparis Date: Mon, 16 Jun 2025 16:54:08 +0200 Subject: [PATCH] installer: reuse existing accounts --- liana-gui/src/installer/message.rs | 2 + liana-gui/src/installer/mod.rs | 6 +- liana-gui/src/installer/step/backend.rs | 102 +++++++++++++++++- liana-gui/src/installer/view/mod.rs | 50 ++++++--- .../src/services/connect/client/cache.rs | 28 ++--- 5 files changed, 159 insertions(+), 29 deletions(-) diff --git a/liana-gui/src/installer/message.rs b/liana-gui/src/installer/message.rs index 5be74a0f..693da779 100644 --- a/liana-gui/src/installer/message.rs +++ b/liana-gui/src/installer/message.rs @@ -102,6 +102,8 @@ pub enum SelectBackend { // Commands messages OTPRequested(Result<(AuthClient, String), Error>), OTPResent(Result<(), Error>), + ExistingConnectAccounts(Vec), + SelectConnectAccount(String), Connected(Result), } diff --git a/liana-gui/src/installer/mod.rs b/liana-gui/src/installer/mod.rs index 52cc69b3..de7bc34e 100644 --- a/liana-gui/src/installer/mod.rs +++ b/liana-gui/src/installer/mod.rs @@ -137,7 +137,8 @@ impl Installer { BackupDescriptor::default().into(), RegisterDescriptor::new_create_wallet().into(), ChooseBackend::new(network).into(), - RemoteBackendLogin::new(network).into(), + RemoteBackendLogin::new(network, destination_path.network_directory(network)) + .into(), SelectBitcoindTypeStep::new().into(), InternalBitcoindStep::new(&context.liana_directory).into(), DefineNode::default().into(), @@ -147,7 +148,8 @@ impl Installer { UserFlow::ShareXpubs => vec![ShareXpubs::new(network, signer.clone()).into()], UserFlow::AddWallet => vec![ ChooseBackend::new(network).into(), - RemoteBackendLogin::new(network).into(), + RemoteBackendLogin::new(network, destination_path.network_directory(network)) + .into(), ImportRemoteWallet::new(network).into(), ImportDescriptor::new(network).into(), RecoverMnemonic::default().into(), diff --git a/liana-gui/src/installer/step/backend.rs b/liana-gui/src/installer/step/backend.rs index 8ac2361d..c5379953 100644 --- a/liana-gui/src/installer/step/backend.rs +++ b/liana-gui/src/installer/step/backend.rs @@ -7,6 +7,7 @@ use liana_ui::{component::form, widget::Element}; use crate::{ daemon::DaemonError, + dir::NetworkDirectory, hw::HardwareWallets, installer::{ context::{self, Context, RemoteBackend}, @@ -18,6 +19,7 @@ use crate::{ self, auth::{AuthClient, AuthError}, backend::{api, BackendClient}, + cache, }, }; @@ -97,6 +99,8 @@ pub enum ConnectionStep { pub struct RemoteBackendLogin { network: Network, + network_dir: NetworkDirectory, + connect_accounts: Vec, processing: bool, step: ConnectionStep, connection_error: Option, @@ -104,9 +108,11 @@ pub struct RemoteBackendLogin { } impl RemoteBackendLogin { - pub fn new(network: Network) -> Self { + pub fn new(network: Network, network_dir: NetworkDirectory) -> Self { Self { network, + network_dir, + connect_accounts: Vec::new(), step: ConnectionStep::EnterEmail { email: form::Value::default(), }, @@ -140,6 +146,21 @@ impl Step for RemoteBackendLogin { .is_ok(); email.value = value; } + Message::SelectBackend(message::SelectBackend::ExistingConnectAccounts( + accounts, + )) => { + self.connect_accounts = accounts; + } + Message::SelectBackend(message::SelectBackend::SelectConnectAccount(email)) => { + return Task::perform( + connect_with_existing_account( + email, + self.network, + self.network_dir.clone(), + ), + |msg| Message::SelectBackend(message::SelectBackend::Connected(msg)), + ) + } Message::SelectBackend(message::SelectBackend::RequestOTP) => { if email.value.is_empty() { email.valid = false; @@ -189,6 +210,32 @@ impl Step for RemoteBackendLogin { } } } + Message::SelectBackend(message::SelectBackend::Connected(res)) => { + self.processing = false; + match res { + Ok(remote_backend) => { + self.step = ConnectionStep::Connected { + email: remote_backend + .user_email() + .expect("Gui connected to Liana backend") + .to_string(), + remote_backend, + }; + return Task::perform(async move {}, |_| Message::Next); + } + Err(e) => { + if let Error::Auth(AuthError { http_status, .. }) = e { + if http_status == Some(403) { + self.auth_error = Some("Token has expired or is invalid") + } else { + self.connection_error = Some(e); + } + } else { + self.connection_error = Some(e); + } + } + } + } _ => {} }, ConnectionStep::EnterOtp { @@ -243,7 +290,6 @@ impl Step for RemoteBackendLogin { .map(Message::SelectBackend); } } - Message::SelectBackend(message::SelectBackend::Connected(res)) => { self.processing = false; match res { @@ -281,6 +327,27 @@ impl Step for RemoteBackendLogin { Task::none() } + fn load(&self) -> Task { + if let Ok(cache) = cache::ConnectCache::from_file(&self.network_dir) { + Task::perform( + async move { + cache + .accounts + .into_iter() + .map(|a| a.email) + .collect::>() + }, + |accounts| { + Message::SelectBackend(message::SelectBackend::ExistingConnectAccounts( + accounts, + )) + }, + ) + } else { + Task::none() + } + } + fn apply(&mut self, ctx: &mut Context) -> bool { if let ConnectionStep::Connected { remote_backend, .. } = &self.step { ctx.remote_backend = remote_backend.clone(); @@ -310,6 +377,7 @@ impl Step for RemoteBackendLogin { email, self.processing, self.connection_error.as_ref(), + &self.connect_accounts, self.auth_error, ), ConnectionStep::EnterOtp { email, otp, .. } => view::connection_step_enter_otp( @@ -330,6 +398,36 @@ impl Step for RemoteBackendLogin { } } +pub async fn connect_with_existing_account( + email: String, + network: Network, + network_dir: NetworkDirectory, +) -> Result { + let config = client::get_service_config(network).await.map_err(|e| { + if e.status() == Some(reqwest::StatusCode::NOT_FOUND) { + Error::Unexpected("Remote servers are unresponsive".to_string()) + } else { + Error::Unexpected(e.to_string()) + } + })?; + + let client = AuthClient::new(config.auth_api_url, config.auth_api_public_key, email); + + let mut tokens = cache::Account::from_cache(&network_dir, &client.email) + .map_err(|_| Error::Unexpected("Account must be in cache".to_string()))? + .ok_or(Error::Unexpected("Account must be in cache".to_string()))? + .tokens; + + if tokens.expires_at < chrono::Utc::now().timestamp() { + tokens = cache::update_connect_cache(&network_dir, &tokens, &client, true) + .await + .map_err(|e| Error::Unexpected(format!("Failed to update cache: {}", e)))?; + } + + let client = BackendClient::connect(client, config.backend_api_url, tokens, network).await?; + Ok(RemoteBackend::WithoutWallet(client)) +} + pub async fn connect( auth: AuthClient, token: String, diff --git a/liana-gui/src/installer/view/mod.rs b/liana-gui/src/installer/view/mod.rs index 86dbf353..c325982a 100644 --- a/liana-gui/src/installer/view/mod.rs +++ b/liana-gui/src/installer/view/mod.rs @@ -2102,18 +2102,40 @@ pub fn login(progress: (usize, usize), connection_step: Element) -> Ele } pub fn connection_step_enter_email<'a>( - email: &form::Value, + email: &'a form::Value, processing: bool, - connection_error: Option<&Error>, - auth_error: Option<&'static str>, + connection_error: Option<&'a Error>, + accounts: &'a [String], + auth_error: Option<&'a str>, ) -> Element<'a, Message> { Column::new() .spacing(20) + .push_maybe(if !accounts.is_empty() { + Some(text("Choose an account you are already using:")) + } else { + None + }) + .push( + accounts + .iter() + .fold(Row::new().spacing(10), |row, a| { + row.push( + Button::new(Container::new(p1_regular(a)).padding(5)) + .style(theme::button::secondary) + .on_press(Message::SelectBackend( + message::SelectBackend::SelectConnectAccount(a.clone()), + )), + ) + }) + .wrap(), + ) .push_maybe(connection_error.map(|e| text(e.to_string()).style(theme::text::warning))) .push_maybe(auth_error.map(|e| text(e.to_string()).style(theme::text::warning))) - .push(text( - "Enter the email you want to associate with the wallet:", - )) + .push(if accounts.is_empty() { + text("Enter an email you want to associate with the wallet:") + } else { + text("Or enter a new email you want to associate with the wallet:") + }) .push( form::Form::new_trimmed("email", email, |msg| { Message::SelectBackend(message::SelectBackend::EmailEdited(msg)) @@ -2123,13 +2145,15 @@ pub fn connection_step_enter_email<'a>( .warning("Email is not valid"), ) .push( - button::secondary(None, "Next") - .on_press_maybe(if processing || !email.valid { - None - } else { - Some(Message::SelectBackend(message::SelectBackend::RequestOTP)) - }) - .width(Length::Fixed(200.0)), + Row::new().push(Space::with_width(Length::Fill)).push( + button::secondary(None, "Send token") + .on_press_maybe(if processing || !email.valid { + None + } else { + Some(Message::SelectBackend(message::SelectBackend::RequestOTP)) + }) + .width(Length::Fixed(200.0)), + ), ) .into() } diff --git a/liana-gui/src/services/connect/client/cache.rs b/liana-gui/src/services/connect/client/cache.rs index 610ac985..d4bb2f0e 100644 --- a/liana-gui/src/services/connect/client/cache.rs +++ b/liana-gui/src/services/connect/client/cache.rs @@ -27,19 +27,8 @@ impl ConnectCache { }) } } -} -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct Account { - pub email: String, - pub tokens: AccessTokenResponse, -} - -impl Account { - pub fn from_cache( - network_dir: &NetworkDirectory, - email: &str, - ) -> Result, ConnectCacheError> { + pub fn from_file(network_dir: &NetworkDirectory) -> Result { let mut path = network_dir.path().to_path_buf(); path.push(CONNECT_CACHE_FILENAME); @@ -53,6 +42,21 @@ impl Account { ConnectCacheError::ReadingFile(format!("Parsing settings file: {}", e)) }) }) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Account { + pub email: String, + pub tokens: AccessTokenResponse, +} + +impl Account { + pub fn from_cache( + network_dir: &NetworkDirectory, + email: &str, + ) -> Result, ConnectCacheError> { + ConnectCache::from_file(network_dir) .map(|cache| cache.accounts.into_iter().find(|c| c.email == email)) } }