Merge #1683: Add connect cache file
fe14c7807e60a080052ebc94c1e0d160c8db8ed6 pin email in liana connect login (edouardparis)
1746314e7d8ef4e06cc05951dc620d88d280d26e Add liana-connect cache file (edouardparis)
Pull request description:
This PR introduces a new file in the network directory: `connect.json` where the access tokens per email accounts are stored. Race conditions and potential conflicts to update the file are handle by the crate `async-fd-lock`.
If the cache is missing (like for a previous liana generated setup), then the user is asked to authenticate again.
The change does not remove totally the legacy `refresh_token` field in the settings file in order to be backward compatible with previous liana-gui versions.
ACKs for top commit:
edouardparis:
Self-ACK fe14c7807e60a080052ebc94c1e0d160c8db8ed6
Tree-SHA512: a4ce6c78e8edd8378f227ae464a5ff0ebd88ee16b519a26f78eb95c9ab5752d14a50ea559902ce5edb9a00452e77b2552c2743a75182a9de825846b5c6f4d06c
This commit is contained in:
commit
6fe88ad4df
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -254,6 +254,21 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fd-lock"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7569377d7062165f6f7834d9cb3051974a2d141433cc201c2f94c149e993cccf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"cfg-if",
|
||||
"pin-project",
|
||||
"rustix",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.1.2"
|
||||
@ -2993,6 +3008,7 @@ dependencies = [
|
||||
name = "liana-gui"
|
||||
version = "10.0.0"
|
||||
dependencies = [
|
||||
"async-fd-lock",
|
||||
"async-hwi",
|
||||
"async-trait",
|
||||
"backtrace",
|
||||
|
||||
@ -29,6 +29,7 @@ iced_runtime = "0.13.1"
|
||||
email_address = "0.2.7"
|
||||
|
||||
tokio = {version = "1.21.0", features = ["signal"]}
|
||||
async-fd-lock = "0.2.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
|
||||
@ -66,7 +66,21 @@ impl Settings {
|
||||
pub struct AuthConfig {
|
||||
pub email: String,
|
||||
pub wallet_id: String,
|
||||
pub refresh_token: String,
|
||||
// legacy field, refresh_token is now stored in the connect cache file
|
||||
// Keep it in case, user want to open the wallet with a previous Liana-GUI version.
|
||||
// Field cannot be ignored as the settings file is override during settings update.
|
||||
// TODO: remove later after multiple versions.
|
||||
pub refresh_token: Option<String>,
|
||||
}
|
||||
|
||||
impl AuthConfig {
|
||||
pub fn new(email: String, wallet_id: String) -> Self {
|
||||
Self {
|
||||
email,
|
||||
wallet_id,
|
||||
refresh_token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
@ -241,7 +255,6 @@ pub enum SettingsError {
|
||||
WritingFile(String),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SettingsError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
|
||||
@ -13,6 +13,7 @@ use liana_ui::{
|
||||
widget::{Column, Element},
|
||||
};
|
||||
use lianad::config::Config;
|
||||
use std::ops::Deref;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use std::io::Write;
|
||||
@ -37,6 +38,7 @@ use crate::{
|
||||
api::payload::{Provider, ProviderKey},
|
||||
BackendClient, BackendWalletClient,
|
||||
},
|
||||
cache::update_connect_cache,
|
||||
},
|
||||
},
|
||||
signer::Signer,
|
||||
@ -550,6 +552,22 @@ pub async fn create_remote_wallet(
|
||||
|
||||
info!("Settings file created");
|
||||
|
||||
let backend = remote_backend.inner_client();
|
||||
if let Err(e) = update_connect_cache(
|
||||
&network_datadir,
|
||||
backend.auth.read().await.deref(),
|
||||
backend.auth_client(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// this error is not critical, the liana-connect backend stored the wallet
|
||||
// and user can reauthenticate.
|
||||
tracing::error!("Failed to update Liana-Connect cache: {}", e);
|
||||
} else {
|
||||
info!("Liana-Connect cache updated");
|
||||
};
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
@ -599,6 +617,22 @@ pub async fn import_remote_wallet(
|
||||
|
||||
info!("Gui configuration file created");
|
||||
|
||||
let backend = backend.inner_client();
|
||||
if let Err(e) = update_connect_cache(
|
||||
&network_datadir,
|
||||
backend.auth.read().await.deref(),
|
||||
backend.auth_client(),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
{
|
||||
// this error is not critical, the liana-connect backend stored the wallet
|
||||
// and user can reauthenticate.
|
||||
tracing::error!("Failed to update Liana-Connect cache: {}", e);
|
||||
} else {
|
||||
info!("Liana-Connect cache updated");
|
||||
};
|
||||
|
||||
Ok(gui_config_path)
|
||||
}
|
||||
|
||||
@ -631,19 +665,16 @@ pub async fn extract_remote_gui_settings(ctx: &Context, backend: &BackendWalletC
|
||||
.expect("LianaDescriptor.to_string() always include the checksum")
|
||||
.to_string();
|
||||
|
||||
let auth = backend.inner_client().auth.read().await;
|
||||
|
||||
Settings {
|
||||
wallets: vec![WalletSetting {
|
||||
name: wallet_name(descriptor),
|
||||
descriptor_checksum,
|
||||
keys: Vec::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
remote_backend_auth: Some(AuthConfig {
|
||||
email: backend.user_email().to_string(),
|
||||
wallet_id: backend.wallet_id(),
|
||||
refresh_token: auth.refresh_token.clone(),
|
||||
}),
|
||||
remote_backend_auth: Some(AuthConfig::new(
|
||||
backend.user_email().to_string(),
|
||||
backend.wallet_id(),
|
||||
)),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
@ -223,14 +223,13 @@ impl GUI {
|
||||
);
|
||||
let network_dir = datadir_path.network_directory(network);
|
||||
if let Ok(settings) = app::settings::Settings::from_file(&network_dir) {
|
||||
if settings
|
||||
if let Some(setting) = settings
|
||||
.wallets
|
||||
.first()
|
||||
.map(|w| w.remote_backend_auth.is_some())
|
||||
== Some(true)
|
||||
.into_iter()
|
||||
.find_map(|w| w.remote_backend_auth)
|
||||
{
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(datadir_path, network, settings);
|
||||
login::LianaLiteLogin::new(datadir_path, network, setting);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
@ -296,14 +295,13 @@ impl GUI {
|
||||
let network_dir = i.datadir.network_directory(i.network);
|
||||
let settings = app::settings::Settings::from_file(&network_dir)
|
||||
.expect("A settings file was created");
|
||||
if settings
|
||||
if let Some(setting) = settings
|
||||
.wallets
|
||||
.first()
|
||||
.map(|w| w.remote_backend_auth.is_some())
|
||||
== Some(true)
|
||||
.into_iter()
|
||||
.find_map(|w| w.remote_backend_auth)
|
||||
{
|
||||
let (login, command) =
|
||||
login::LianaLiteLogin::new(i.datadir.clone(), i.network, settings);
|
||||
login::LianaLiteLogin::new(i.datadir.clone(), i.network, setting);
|
||||
self.state = State::Login(Box::new(login));
|
||||
command.map(|msg| Message::Login(Box::new(msg)))
|
||||
} else {
|
||||
|
||||
@ -20,7 +20,6 @@ use reqwest::{Error, IntoUrl, Method, RequestBuilder, Response};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{
|
||||
app::settings::{AuthConfig, Settings},
|
||||
daemon::{model::*, Daemon, DaemonBackend, DaemonError},
|
||||
dir::LianaDirectory,
|
||||
hw::HardwareWalletConfig,
|
||||
@ -28,7 +27,10 @@ use crate::{
|
||||
|
||||
use self::api::{UTXOKind, DEFAULT_OUTPOINTS_LIMIT};
|
||||
|
||||
use super::auth::{self, AccessTokenResponse, AuthError};
|
||||
use super::{
|
||||
auth::{self, AccessTokenResponse, AuthError},
|
||||
cache::update_connect_cache,
|
||||
};
|
||||
|
||||
impl From<Error> for DaemonError {
|
||||
fn from(value: Error) -> Self {
|
||||
@ -101,6 +103,10 @@ impl BackendClient {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn auth_client(&self) -> &auth::AuthClient {
|
||||
&self.auth_client
|
||||
}
|
||||
|
||||
pub fn user_email(&self) -> &str {
|
||||
&self.auth_client.email
|
||||
}
|
||||
@ -532,45 +538,20 @@ impl Daemon for BackendWalletClient {
|
||||
return Ok(());
|
||||
}
|
||||
Ok(mut old) => {
|
||||
let new = self
|
||||
.inner
|
||||
.auth_client
|
||||
.refresh_token(&auth.refresh_token)
|
||||
.await?;
|
||||
|
||||
let network_dir = datadir.network_directory(network);
|
||||
let mut settings = Settings::from_file(&network_dir).map_err(|e| {
|
||||
DaemonError::Unexpected(format!(
|
||||
"Cannot access to settings.json file: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
|
||||
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
|
||||
if let Some(auth) = &w.remote_backend_auth {
|
||||
auth.wallet_id == self.wallet_uuid
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
wallet_settings.remote_backend_auth = Some(AuthConfig {
|
||||
email: self.inner.auth_client.email.clone(),
|
||||
wallet_id: self.wallet_id(),
|
||||
refresh_token: new.refresh_token.clone(),
|
||||
});
|
||||
} else {
|
||||
tracing::info!("Wallet id was not found in the settings");
|
||||
}
|
||||
|
||||
settings.to_file(&network_dir).map_err(|e| {
|
||||
DaemonError::Unexpected(format!(
|
||||
"Cannot access to settings.json file: {}",
|
||||
e
|
||||
))
|
||||
let new = update_connect_cache(
|
||||
&network_dir,
|
||||
&old,
|
||||
&self.inner.auth_client,
|
||||
true, // refresh the token
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
DaemonError::Unexpected(format!("Cannot update Liana-connect cache: {}", e))
|
||||
})?;
|
||||
|
||||
*old = new;
|
||||
tracing::info!("Liana backend access was refreshed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
159
liana-gui/src/services/connect/client/cache.rs
Normal file
159
liana-gui/src/services/connect/client/cache.rs
Normal file
@ -0,0 +1,159 @@
|
||||
use crate::dir::NetworkDirectory;
|
||||
use async_fd_lock::LockWrite;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::SeekFrom;
|
||||
use tokio::fs::OpenOptions;
|
||||
use tokio::io::AsyncSeekExt;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use super::auth::{AccessTokenResponse, AuthClient, AuthError};
|
||||
|
||||
pub const CONNECT_CACHE_FILENAME: &str = "connect.json";
|
||||
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct ConnectCache {
|
||||
pub accounts: Vec<Account>,
|
||||
}
|
||||
|
||||
impl ConnectCache {
|
||||
fn upsert_credential(&mut self, email: &str, tokens: AccessTokenResponse) {
|
||||
if let Some(c) = self.accounts.iter_mut().find(|c| c.email == email) {
|
||||
c.tokens = tokens;
|
||||
} else {
|
||||
self.accounts.push(Account {
|
||||
email: email.to_string(),
|
||||
tokens,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Option<Self>, ConnectCacheError> {
|
||||
let mut path = network_dir.path().to_path_buf();
|
||||
path.push(CONNECT_CACHE_FILENAME);
|
||||
|
||||
std::fs::read(path)
|
||||
.map_err(|e| match e.kind() {
|
||||
std::io::ErrorKind::NotFound => ConnectCacheError::NotFound,
|
||||
_ => ConnectCacheError::ReadingFile(format!("Reading settings file: {}", e)),
|
||||
})
|
||||
.and_then(|file_content| {
|
||||
serde_json::from_slice::<ConnectCache>(&file_content).map_err(|e| {
|
||||
ConnectCacheError::ReadingFile(format!("Parsing settings file: {}", e))
|
||||
})
|
||||
})
|
||||
.map(|cache| cache.accounts.into_iter().find(|c| c.email == email))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_connect_cache(
|
||||
network_dir: &NetworkDirectory,
|
||||
current_tokens: &AccessTokenResponse,
|
||||
client: &AuthClient,
|
||||
refresh: bool,
|
||||
) -> Result<AccessTokenResponse, ConnectCacheError> {
|
||||
let email = &client.email;
|
||||
let mut path = network_dir.path().to_path_buf();
|
||||
path.push(CONNECT_CACHE_FILENAME);
|
||||
|
||||
let file_exists = tokio::fs::try_exists(&path).await.unwrap_or(false);
|
||||
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&path)
|
||||
.await
|
||||
.map_err(|e| ConnectCacheError::ReadingFile(format!("Opening file: {}", e)))?
|
||||
.lock_write()
|
||||
.await
|
||||
.map_err(|e| ConnectCacheError::ReadingFile(format!("Locking file: {:?}", e)))?;
|
||||
|
||||
let mut cache = if file_exists {
|
||||
let mut file_content = Vec::new();
|
||||
file.read_to_end(&mut file_content)
|
||||
.await
|
||||
.map_err(|e| ConnectCacheError::ReadingFile(format!("Reading file content: {}", e)))?;
|
||||
|
||||
match serde_json::from_slice::<ConnectCache>(&file_content) {
|
||||
Ok(cache) => cache,
|
||||
Err(e) => {
|
||||
tracing::warn!("Something wrong with Liana-Connect cache file: {:?}", e);
|
||||
tracing::warn!("Liana-Connect cache file is reset");
|
||||
ConnectCache::default()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ConnectCache::default()
|
||||
};
|
||||
|
||||
if let Some(c) = cache.accounts.iter().find(|cred| cred.email == *email) {
|
||||
// An other process updated the tokens
|
||||
if current_tokens.expires_at < c.tokens.expires_at {
|
||||
tracing::debug!("Liana-Connect authentication tokens are up to date, nothing to do");
|
||||
return Ok(c.tokens.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let tokens = if refresh {
|
||||
client
|
||||
.refresh_token(¤t_tokens.refresh_token)
|
||||
.await
|
||||
.map_err(ConnectCacheError::Updating)?
|
||||
} else {
|
||||
current_tokens.clone()
|
||||
};
|
||||
|
||||
cache.upsert_credential(email, tokens.clone());
|
||||
|
||||
let content = serde_json::to_vec_pretty(&cache).map_err(|e| {
|
||||
ConnectCacheError::WritingFile(format!("Failed to serialize settings: {}", e))
|
||||
})?;
|
||||
|
||||
file.seek(SeekFrom::Start(0)).await.map_err(|e| {
|
||||
ConnectCacheError::WritingFile(format!("Failed to seek to start of file: {}", e))
|
||||
})?;
|
||||
|
||||
file.write_all(&content).await.map_err(|e| {
|
||||
tracing::warn!("failed to write to file: {:?}", e);
|
||||
ConnectCacheError::WritingFile(e.to_string())
|
||||
})?;
|
||||
|
||||
file.inner_mut()
|
||||
.set_len(content.len() as u64)
|
||||
.await
|
||||
.map_err(|e| ConnectCacheError::WritingFile(format!("Failed to truncate file: {}", e)))?;
|
||||
|
||||
Ok(tokens)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ConnectCacheError {
|
||||
NotFound,
|
||||
ReadingFile(String),
|
||||
WritingFile(String),
|
||||
Unexpected(String),
|
||||
Updating(AuthError),
|
||||
}
|
||||
impl std::fmt::Display for ConnectCacheError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::NotFound => write!(f, "ConnectCache file not found"),
|
||||
Self::ReadingFile(e) => write!(f, "Error while reading file: {}", e),
|
||||
Self::WritingFile(e) => write!(f, "Error while writing file: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
Self::Updating(e) => write!(f, "Error while updating cache file: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod auth;
|
||||
pub mod backend;
|
||||
pub mod cache;
|
||||
|
||||
use liana::miniscript::bitcoin;
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ use lianad::commands::ListCoinsResult;
|
||||
use crate::{
|
||||
app::{
|
||||
cache::coins_to_cache,
|
||||
settings::{AuthConfig, Settings, SettingsError, WalletSetting},
|
||||
settings::{AuthConfig, SettingsError},
|
||||
},
|
||||
daemon::DaemonError,
|
||||
dir::LianaDirectory,
|
||||
@ -22,15 +22,18 @@ use crate::{
|
||||
use super::client::{
|
||||
auth::{AuthClient, AuthError},
|
||||
backend::{api, BackendClient, BackendWalletClient},
|
||||
cache::{self, update_connect_cache, ConnectCacheError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
Auth(AuthError),
|
||||
CredentialsMissing,
|
||||
// DaemonError does not implement Clone.
|
||||
// TODO: maybe Arc is overkill
|
||||
Backend(Arc<DaemonError>),
|
||||
Settings(SettingsError),
|
||||
Cache(cache::ConnectCacheError),
|
||||
Unexpected(String),
|
||||
}
|
||||
|
||||
@ -38,8 +41,10 @@ impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Auth(e) => write!(f, "Authentication error: {}", e),
|
||||
Self::CredentialsMissing => write!(f, "credentials missing"),
|
||||
Self::Backend(e) => write!(f, "Remote backend error: {}", e),
|
||||
Self::Settings(e) => write!(f, "Settings file error: {}", e),
|
||||
Self::Cache(e) => write!(f, "Connect cache file error: {}", e),
|
||||
Self::Unexpected(e) => write!(f, "Unexpected error: {}", e),
|
||||
}
|
||||
}
|
||||
@ -63,6 +68,12 @@ impl From<SettingsError> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConnectCacheError> for Error {
|
||||
fn from(value: ConnectCacheError) -> Self {
|
||||
Self::Cache(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
View(ViewMessage),
|
||||
@ -88,8 +99,6 @@ pub enum Message {
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ViewMessage {
|
||||
RequestOTP,
|
||||
EditEmail,
|
||||
EmailEdited(String),
|
||||
OTPEdited(String),
|
||||
BackToLauncher(Network),
|
||||
}
|
||||
@ -105,6 +114,7 @@ pub struct LianaLiteLogin {
|
||||
pub network: Network,
|
||||
|
||||
wallet_id: String,
|
||||
email: String,
|
||||
|
||||
processing: bool,
|
||||
step: ConnectionStep,
|
||||
@ -117,13 +127,10 @@ pub struct LianaLiteLogin {
|
||||
|
||||
pub enum ConnectionStep {
|
||||
CheckingAuthFile,
|
||||
EnterEmail {
|
||||
email: form::Value<String>,
|
||||
},
|
||||
CheckEmail,
|
||||
EnterOtp {
|
||||
client: AuthClient,
|
||||
backend_api_url: String,
|
||||
email: String,
|
||||
otp: form::Value<String>,
|
||||
},
|
||||
}
|
||||
@ -132,63 +139,41 @@ impl LianaLiteLogin {
|
||||
pub fn new(
|
||||
datadir: LianaDirectory,
|
||||
network: Network,
|
||||
settings: Settings,
|
||||
setting: AuthConfig,
|
||||
) -> (Self, Task<Message>) {
|
||||
match settings
|
||||
.wallets
|
||||
.first()
|
||||
.cloned()
|
||||
.and_then(|w| w.remote_backend_auth)
|
||||
.ok_or(Error::Unexpected(
|
||||
"Missing auth configuration in settings.json".to_string(),
|
||||
)) {
|
||||
Err(e) => (
|
||||
Self {
|
||||
network,
|
||||
datadir,
|
||||
step: ConnectionStep::EnterEmail {
|
||||
email: form::Value::default(),
|
||||
},
|
||||
wallet_id: String::new(),
|
||||
connection_error: Some(e),
|
||||
auth_error: None,
|
||||
processing: true,
|
||||
},
|
||||
Task::none(),
|
||||
),
|
||||
Ok(auth_config) => (
|
||||
Self {
|
||||
network,
|
||||
datadir,
|
||||
step: ConnectionStep::CheckingAuthFile,
|
||||
connection_error: None,
|
||||
wallet_id: auth_config.wallet_id.clone(),
|
||||
auth_error: None,
|
||||
processing: true,
|
||||
},
|
||||
Task::perform(
|
||||
async move {
|
||||
let service_config = super::client::get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
let client = AuthClient::new(
|
||||
service_config.auth_api_url,
|
||||
service_config.auth_api_public_key,
|
||||
auth_config.email,
|
||||
);
|
||||
connect_with_refresh_token(
|
||||
client,
|
||||
auth_config.refresh_token,
|
||||
auth_config.wallet_id,
|
||||
service_config.backend_api_url,
|
||||
network,
|
||||
)
|
||||
(
|
||||
Self {
|
||||
network,
|
||||
datadir: datadir.clone(),
|
||||
step: ConnectionStep::CheckingAuthFile,
|
||||
connection_error: None,
|
||||
wallet_id: setting.wallet_id.clone(),
|
||||
email: setting.email.clone(),
|
||||
auth_error: None,
|
||||
processing: true,
|
||||
},
|
||||
Task::perform(
|
||||
async move {
|
||||
let service_config = super::client::get_service_config(network)
|
||||
.await
|
||||
},
|
||||
Message::Connected,
|
||||
),
|
||||
.map_err(|e| Error::Unexpected(e.to_string()))?;
|
||||
let client = AuthClient::new(
|
||||
service_config.auth_api_url,
|
||||
service_config.auth_api_public_key,
|
||||
setting.email,
|
||||
);
|
||||
connect_with_credentials(
|
||||
client,
|
||||
setting.wallet_id,
|
||||
service_config.backend_api_url,
|
||||
network,
|
||||
datadir,
|
||||
)
|
||||
.await
|
||||
},
|
||||
Message::Connected,
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update(&mut self, message: Message) -> Task<Message> {
|
||||
@ -207,36 +192,27 @@ impl LianaLiteLogin {
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
self.connection_error = Some(e);
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value::default(),
|
||||
};
|
||||
// Do not display error, if the Liana-Connect cache does not exist,
|
||||
// simply ask user to do the authentication steps.
|
||||
if !matches!(e, Error::CredentialsMissing) {
|
||||
self.connection_error = Some(e);
|
||||
}
|
||||
self.step = ConnectionStep::CheckEmail;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ConnectionStep::EnterEmail { email } => match message {
|
||||
Message::View(ViewMessage::EmailEdited(value)) => {
|
||||
email.valid = value.is_empty()
|
||||
|| email_address::EmailAddress::parse_with_options(
|
||||
&value,
|
||||
email_address::Options::default().with_required_tld(),
|
||||
)
|
||||
.is_ok();
|
||||
email.value = value;
|
||||
}
|
||||
ConnectionStep::CheckEmail => match message {
|
||||
Message::View(ViewMessage::RequestOTP) => {
|
||||
if email.value.is_empty() {
|
||||
email.valid = false;
|
||||
} else if email.valid {
|
||||
let email = email.value.clone();
|
||||
let network = self.network;
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Task::perform(
|
||||
async move {
|
||||
let config = super::client::get_service_config(network)
|
||||
let email = self.email.clone();
|
||||
let network = self.network;
|
||||
self.processing = true;
|
||||
self.connection_error = None;
|
||||
self.auth_error = None;
|
||||
return Task::perform(
|
||||
async move {
|
||||
let config =
|
||||
super::client::get_service_config(network)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if e.status() == Some(reqwest::StatusCode::NOT_FOUND) {
|
||||
@ -247,24 +223,22 @@ impl LianaLiteLogin {
|
||||
Error::Unexpected(e.to_string())
|
||||
}
|
||||
})?;
|
||||
let client = AuthClient::new(
|
||||
config.auth_api_url,
|
||||
config.auth_api_public_key,
|
||||
email,
|
||||
);
|
||||
client.sign_in_otp().await?;
|
||||
Ok((client, config.backend_api_url))
|
||||
},
|
||||
Message::OTPRequested,
|
||||
);
|
||||
}
|
||||
let client = AuthClient::new(
|
||||
config.auth_api_url,
|
||||
config.auth_api_public_key,
|
||||
email,
|
||||
);
|
||||
client.sign_in_otp().await?;
|
||||
Ok((client, config.backend_api_url))
|
||||
},
|
||||
Message::OTPRequested,
|
||||
);
|
||||
}
|
||||
Message::OTPRequested(res) => {
|
||||
self.processing = false;
|
||||
match res {
|
||||
Ok((client, backend_api_url)) => {
|
||||
self.step = ConnectionStep::EnterOtp {
|
||||
email: email.value.to_owned(),
|
||||
otp: form::Value::default(),
|
||||
client,
|
||||
backend_api_url,
|
||||
@ -279,18 +253,9 @@ impl LianaLiteLogin {
|
||||
},
|
||||
ConnectionStep::EnterOtp {
|
||||
client,
|
||||
email,
|
||||
otp,
|
||||
backend_api_url,
|
||||
} => match message {
|
||||
Message::View(ViewMessage::EditEmail) => {
|
||||
self.step = ConnectionStep::EnterEmail {
|
||||
email: form::Value {
|
||||
value: email.clone(),
|
||||
valid: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
Message::View(ViewMessage::RequestOTP) => {
|
||||
*otp = form::Value::default();
|
||||
let client = client.clone();
|
||||
@ -327,9 +292,11 @@ impl LianaLiteLogin {
|
||||
self.auth_error = None;
|
||||
let wallet_id = self.wallet_id.clone();
|
||||
let network = self.network;
|
||||
let datadir = self.datadir.clone();
|
||||
return Task::perform(
|
||||
async move {
|
||||
connect(client, otp, wallet_id, backend_api_url, network).await
|
||||
connect(client, otp, wallet_id, backend_api_url, network, datadir)
|
||||
.await
|
||||
},
|
||||
Message::Connected,
|
||||
);
|
||||
@ -343,21 +310,8 @@ impl LianaLiteLogin {
|
||||
return Task::perform(async move { Some(client) }, Message::Install);
|
||||
}
|
||||
Ok(BackendState::WalletExists(client, wallet, coins)) => {
|
||||
let datadir = self.datadir.clone();
|
||||
let network = self.network;
|
||||
return Task::perform(
|
||||
async move {
|
||||
update_wallet_auth_settings(
|
||||
datadir,
|
||||
network,
|
||||
wallet.clone(),
|
||||
client.user_email().to_string(),
|
||||
client.auth().await.refresh_token,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok((client, wallet, coins))
|
||||
},
|
||||
async move { Ok((client, wallet, coins)) },
|
||||
Message::Run,
|
||||
);
|
||||
}
|
||||
@ -390,88 +344,66 @@ impl LianaLiteLogin {
|
||||
pub fn view(&self) -> Element<Message> {
|
||||
let content = Into::<Element<ViewMessage>>::into(
|
||||
Container::new(
|
||||
Column::new()
|
||||
.spacing(100)
|
||||
.align_x(Alignment::Center)
|
||||
.push(
|
||||
Column::new()
|
||||
.align_x(Alignment::Center)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.push(h2("Liana Connect"))
|
||||
.push(
|
||||
Column::new()
|
||||
.max_width(500)
|
||||
.spacing(20)
|
||||
.push(match &self.step {
|
||||
ConnectionStep::CheckingAuthFile => Column::new(),
|
||||
ConnectionStep::EnterEmail { email } => Column::new()
|
||||
.spacing(20)
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(theme::text::warning)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new_trimmed("email", email, |msg| {
|
||||
ViewMessage::EmailEdited(msg)
|
||||
})
|
||||
.size(P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Email is not valid"),
|
||||
)
|
||||
.push(button::secondary(None, "Next").on_press_maybe(
|
||||
if self.processing {
|
||||
Column::new().spacing(100).align_x(Alignment::Center).push(
|
||||
Column::new()
|
||||
.align_x(Alignment::Center)
|
||||
.spacing(20)
|
||||
.width(Length::Fill)
|
||||
.push(h2("Liana Connect"))
|
||||
.push(
|
||||
Column::new()
|
||||
.max_width(500)
|
||||
.spacing(20)
|
||||
.push(match &self.step {
|
||||
ConnectionStep::CheckingAuthFile => Column::new(),
|
||||
ConnectionStep::CheckEmail => Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(theme::text::warning)),
|
||||
)
|
||||
.push(text(&self.email))
|
||||
.push(
|
||||
button::secondary(None, "Login")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if self.processing {
|
||||
None
|
||||
} else {
|
||||
Some(ViewMessage::RequestOTP)
|
||||
},
|
||||
)),
|
||||
ConnectionStep::EnterOtp { otp, .. } => Column::new()
|
||||
.push(text("An authentication was send to your email"))
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(theme::text::warning)),
|
||||
)
|
||||
.spacing(20)
|
||||
.push(
|
||||
form::Form::new_trimmed("Token", otp, |msg| {
|
||||
ViewMessage::OTPEdited(msg)
|
||||
})
|
||||
.size(P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Token is not valid"),
|
||||
)
|
||||
.push(
|
||||
Row::new()
|
||||
.spacing(10)
|
||||
.push(
|
||||
button::secondary(
|
||||
Some(icon::previous_icon()),
|
||||
"Change email",
|
||||
)
|
||||
.on_press(ViewMessage::EditEmail),
|
||||
)
|
||||
.push(
|
||||
button::secondary(None, "Resend token")
|
||||
.on_press_maybe(if self.processing {
|
||||
None
|
||||
} else {
|
||||
Some(ViewMessage::RequestOTP)
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
ConnectionStep::EnterOtp { otp, .. } => Column::new()
|
||||
.spacing(20)
|
||||
.align_x(Alignment::Center)
|
||||
.push(text("An authentication was sent to your email:"))
|
||||
.push(text(&self.email))
|
||||
.push_maybe(
|
||||
self.auth_error
|
||||
.map(|e| text(e).style(theme::text::warning)),
|
||||
)
|
||||
.push(
|
||||
form::Form::new_trimmed("Token", otp, |msg| {
|
||||
ViewMessage::OTPEdited(msg)
|
||||
})
|
||||
.size(P1_SIZE)
|
||||
.padding(10)
|
||||
.warning("Token is not valid"),
|
||||
)
|
||||
.push(
|
||||
Row::new().spacing(10).push(
|
||||
button::secondary(None, "Resend token")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press_maybe(if self.processing {
|
||||
None
|
||||
} else {
|
||||
Some(ViewMessage::RequestOTP)
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) {
|
||||
Some(
|
||||
button::secondary(Some(icon::previous_icon()), "Change network")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(ViewMessage::BackToLauncher(self.network)),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.padding(50)
|
||||
.center_x(Length::Fill)
|
||||
@ -490,61 +422,21 @@ impl LianaLiteLogin {
|
||||
);
|
||||
}
|
||||
|
||||
col.push(content).into()
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_wallet_auth_settings(
|
||||
datadir: LianaDirectory,
|
||||
network: Network,
|
||||
wallet: api::Wallet,
|
||||
email: String,
|
||||
refresh_token: String,
|
||||
) -> Result<(), Error> {
|
||||
let network_dir = datadir.network_directory(network);
|
||||
let mut settings = Settings::from_file(&network_dir)?;
|
||||
|
||||
let descriptor_checksum = wallet
|
||||
.descriptor
|
||||
.to_string()
|
||||
.split_once('#')
|
||||
.map(|(_, checksum)| checksum)
|
||||
.expect("Failed to get checksum from a valid LianaDescriptor")
|
||||
.to_string();
|
||||
|
||||
let remote_backend_auth = Some(AuthConfig {
|
||||
email,
|
||||
wallet_id: wallet.id.clone(),
|
||||
refresh_token,
|
||||
});
|
||||
|
||||
if let Some(wallet_settings) = settings.wallets.iter_mut().find(|w| {
|
||||
if let Some(auth) = &w.remote_backend_auth {
|
||||
auth.wallet_id == wallet.id
|
||||
col.push_maybe(if !matches!(self.step, ConnectionStep::CheckingAuthFile) {
|
||||
Some(
|
||||
Container::new(
|
||||
button::secondary(Some(icon::previous_icon()), "Go back")
|
||||
.width(Length::Fixed(200.0))
|
||||
.on_press(Message::View(ViewMessage::BackToLauncher(self.network))),
|
||||
)
|
||||
.padding(20),
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
wallet_settings.remote_backend_auth = remote_backend_auth;
|
||||
} else {
|
||||
tracing::info!("Wallet id was not found in the settings, adding now the wallet settings to the settings.json file");
|
||||
settings.wallets.insert(
|
||||
0,
|
||||
WalletSetting {
|
||||
name: wallet.name,
|
||||
descriptor_checksum,
|
||||
keys: Vec::new(),
|
||||
hardware_wallets: Vec::new(),
|
||||
remote_backend_auth,
|
||||
},
|
||||
);
|
||||
None
|
||||
})
|
||||
.push(content)
|
||||
.into()
|
||||
}
|
||||
|
||||
settings.to_file(&network_dir).map_err(|e| {
|
||||
DaemonError::Unexpected(format!("Cannot access to settings.json file: {}", e))
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
@ -553,9 +445,14 @@ pub async fn connect(
|
||||
wallet_id: String,
|
||||
backend_api_url: String,
|
||||
network: Network,
|
||||
liana_directory: LianaDirectory,
|
||||
) -> Result<BackendState, Error> {
|
||||
let network_dir = liana_directory.network_directory(network);
|
||||
let access = auth.verify_otp(token.trim_end()).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?;
|
||||
let client =
|
||||
BackendClient::connect(auth.clone(), backend_api_url, access.clone(), network).await?;
|
||||
|
||||
update_connect_cache(&network_dir, &access, &auth, false).await?;
|
||||
|
||||
let wallets = client.list_wallets().await?;
|
||||
if wallets.is_empty() {
|
||||
@ -566,25 +463,39 @@ pub async fn connect(
|
||||
let first = wallets.first().cloned().ok_or(DaemonError::NoAnswer)?;
|
||||
let (wallet_client, wallet) = client.connect_wallet(first);
|
||||
let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?;
|
||||
|
||||
Ok(BackendState::WalletExists(wallet_client, wallet, coins))
|
||||
} else if let Some(wallet) = wallets.into_iter().find(|w| w.id == wallet_id) {
|
||||
let (wallet_client, wallet) = client.connect_wallet(wallet);
|
||||
let coins = coins_to_cache(Arc::new(wallet_client.clone())).await?;
|
||||
|
||||
Ok(BackendState::WalletExists(wallet_client, wallet, coins))
|
||||
} else {
|
||||
Ok(BackendState::NoWallet(client))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn connect_with_refresh_token(
|
||||
pub async fn connect_with_credentials(
|
||||
auth: AuthClient,
|
||||
refresh_token: String,
|
||||
wallet_id: String,
|
||||
backend_api_url: String,
|
||||
network: Network,
|
||||
liana_directory: LianaDirectory,
|
||||
) -> Result<BackendState, Error> {
|
||||
let access = auth.refresh_token(&refresh_token).await?;
|
||||
let client = BackendClient::connect(auth, backend_api_url, access.clone(), network).await?;
|
||||
let network_dir = liana_directory.network_directory(network);
|
||||
let mut tokens = cache::Account::from_cache(&network_dir, &auth.email)
|
||||
.map_err(|e| match e {
|
||||
ConnectCacheError::NotFound => Error::CredentialsMissing,
|
||||
_ => e.into(),
|
||||
})?
|
||||
.ok_or(Error::CredentialsMissing)?
|
||||
.tokens;
|
||||
|
||||
if tokens.expires_at < chrono::Utc::now().timestamp() {
|
||||
tokens = cache::update_connect_cache(&network_dir, &tokens, &auth, true).await?;
|
||||
}
|
||||
|
||||
let client = BackendClient::connect(auth, backend_api_url, tokens, network).await?;
|
||||
|
||||
if let Some(wallet) = client
|
||||
.list_wallets()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user