diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-10-01 00:00:00 +0100 |
| commit | e237328ec611a5891586530c1d3cb26c16c1093b (patch) | |
| tree | 22ac36baa240354d06ae82eb070609fa3e3fcb82 /src/login.rs | |
| parent | 000901c0cbca8464b5a89bcc93c5474f6564bafd (diff) | |
feat(login) fetch user relays and metadata
get user relay list and metadata events from relays when keys are
used and last fetch attempt was more than an hour ago
uses user's write relays if known, otherwise uses fallback relays
to achieve this a method for intergration testing event fetching
from relays was added
Diffstat (limited to 'src/login.rs')
| -rw-r--r-- | src/login.rs | 153 |
1 files changed, 97 insertions, 56 deletions
diff --git a/src/login.rs b/src/login.rs index 12fe76e..e73373a 100644 --- a/src/login.rs +++ b/src/login.rs | |||
| @@ -1,10 +1,14 @@ | |||
| 1 | use anyhow::{bail, Context, Result}; | 1 | use anyhow::{bail, Context, Result}; |
| 2 | use nostr::prelude::{FromSkStr, ToBech32}; | 2 | use nostr::{prelude::FromSkStr, secp256k1::XOnlyPublicKey}; |
| 3 | use zeroize::Zeroize; | 3 | use zeroize::Zeroize; |
| 4 | 4 | ||
| 5 | #[cfg(not(test))] | ||
| 6 | use crate::client::Client; | ||
| 7 | #[cfg(test)] | ||
| 8 | use crate::client::MockConnect; | ||
| 5 | use crate::{ | 9 | use crate::{ |
| 6 | cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, | 10 | cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, |
| 7 | config::{ConfigManagement, ConfigManager}, | 11 | config::{ConfigManagement, ConfigManager, UserRef}, |
| 8 | key_handling::{ | 12 | key_handling::{ |
| 9 | encryption::{EncryptDecrypt, Encryptor}, | 13 | encryption::{EncryptDecrypt, Encryptor}, |
| 10 | users::{UserManagement, UserManager}, | 14 | users::{UserManagement, UserManager}, |
| @@ -12,77 +16,114 @@ use crate::{ | |||
| 12 | }; | 16 | }; |
| 13 | 17 | ||
| 14 | /// handles the encrpytion and storage of key material | 18 | /// handles the encrpytion and storage of key material |
| 15 | pub fn launch(nsec: &Option<String>, password: &Option<String>) -> Result<nostr::Keys> { | 19 | pub async fn launch( |
| 20 | nsec: &Option<String>, | ||
| 21 | password: &Option<String>, | ||
| 22 | #[cfg(test)] client: Option<&MockConnect>, | ||
| 23 | #[cfg(not(test))] client: Option<&Client>, | ||
| 24 | ) -> Result<nostr::Keys> { | ||
| 16 | // if nsec parameter | 25 | // if nsec parameter |
| 17 | if let Some(nsec_unwrapped) = nsec { | 26 | let key = if let Some(nsec_unwrapped) = nsec { |
| 18 | // get key or fail without prompts | 27 | // get key or fail without prompts |
| 19 | let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; | 28 | let key = nostr::Keys::from_sk_str(nsec_unwrapped).context("invalid nsec parameter")?; |
| 20 | println!( | ||
| 21 | "logged in as {}", | ||
| 22 | &key.public_key() | ||
| 23 | .to_bech32() | ||
| 24 | .context("public key should always produce bech32")? | ||
| 25 | ); | ||
| 26 | 29 | ||
| 27 | // if password, add user to enable password login in future | 30 | // if password, add user to enable password login in future |
| 28 | if password.is_some() { | 31 | if password.is_some() { |
| 29 | UserManager::default() | 32 | UserManager::default() |
| 30 | .add(nsec, password) | 33 | .add(nsec, password) |
| 31 | .context("could not store identity")?; | 34 | .context("could not store identity")?; |
| 35 | } else { | ||
| 36 | UserManager::default().add_user_to_config(key.public_key(), None, false)?; | ||
| 32 | } | 37 | } |
| 33 | return Ok(key); | 38 | key |
| 34 | } | 39 | } else { |
| 40 | let cfg = ConfigManager | ||
| 41 | .load() | ||
| 42 | .context("failed to load application config")?; | ||
| 43 | // if encrypted nsec present | ||
| 44 | if cfg.users.last().is_some() && !cfg.users.last().unwrap().encrypted_key.is_empty() { | ||
| 45 | // unfortunately this line is unstable in rust: | ||
| 46 | // if let Some(user) = cfg.users.last() && !user.encrypted_key.is_empty() { | ||
| 47 | let user = cfg.users.last().unwrap(); | ||
| 48 | let mut pass = if let Some(p) = password.clone() { | ||
| 49 | p | ||
| 50 | } else { | ||
| 51 | println!("login as {}", &user.metadata.name); | ||
| 52 | Interactor::default() | ||
| 53 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 54 | .context("failed to get password input from interactor.password")? | ||
| 55 | }; | ||
| 35 | 56 | ||
| 36 | // if encrypted nsec stored, attempt password | 57 | let key_result = Encryptor |
| 37 | let cfg = ConfigManager | 58 | .decrypt_key(&user.encrypted_key, pass.as_str()) |
| 38 | .load() | 59 | .context("failed to decrypt key with provided password"); |
| 39 | .context("failed to load application config")?; | 60 | pass.zeroize(); |
| 40 | let key = if let Some(user) = cfg.users.last() { | ||
| 41 | let mut pass = if let Some(p) = password.clone() { | ||
| 42 | p | ||
| 43 | } else { | ||
| 44 | println!( | ||
| 45 | "login as {}", | ||
| 46 | &user | ||
| 47 | .public_key | ||
| 48 | .to_bech32() | ||
| 49 | .context("public key should always produce bech32")? | ||
| 50 | ); | ||
| 51 | Interactor::default() | ||
| 52 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 53 | .context("failed to get password input from interactor.password")? | ||
| 54 | }; | ||
| 55 | 61 | ||
| 56 | let key_result = Encryptor | 62 | key_result.context(format!("failed to log in as {}", &user.metadata.name))? |
| 57 | .decrypt_key(&user.encrypted_key, pass.as_str()) | 63 | } |
| 58 | .context("failed to decrypt key with provided password"); | 64 | // no encrypted nsec present |
| 59 | pass.zeroize(); | 65 | else { |
| 66 | // no nsec but password supplied | ||
| 67 | if password.is_some() { | ||
| 68 | bail!("no nsec available to decrypt with specified password"); | ||
| 69 | } | ||
| 70 | // otherwise add new user with nsec and password prompts | ||
| 71 | UserManager::default() | ||
| 72 | .add(nsec, password) | ||
| 73 | .context("failed to add user")? | ||
| 74 | } | ||
| 75 | }; | ||
| 60 | 76 | ||
| 61 | key_result.context(format!( | 77 | // get user details |
| 62 | "failed to log in as {}", | 78 | let user_ref = if let Some(client) = client { |
| 63 | &user | 79 | get_user_details(&key.public_key(), client).await? |
| 64 | .public_key | ||
| 65 | .to_bech32() | ||
| 66 | .context("public key should always produce bech32")? | ||
| 67 | ))? | ||
| 68 | } else { | 80 | } else { |
| 69 | // no nsec but password supplied | 81 | // this will get user details with name as npub |
| 70 | if password.is_some() { | ||
| 71 | bail!("no nsec available to decrypt with specified password"); | ||
| 72 | } | ||
| 73 | // otherwise add new user with nsec and password prompts | ||
| 74 | UserManager::default() | 82 | UserManager::default() |
| 75 | .add(nsec, password) | 83 | .get_user_from_cache(&key.public_key())? |
| 76 | .context("failed to add user")? | 84 | .clone() |
| 77 | }; | 85 | }; |
| 78 | println!( | ||
| 79 | "logged in as {}", | ||
| 80 | &key.public_key() | ||
| 81 | .to_bech32() | ||
| 82 | .context("public key should always produce bech32")? | ||
| 83 | ); | ||
| 84 | 86 | ||
| 85 | // fetching metdata | 87 | // print logged in |
| 88 | println!("logged in as {}", user_ref.metadata.name); | ||
| 86 | 89 | ||
| 87 | Ok(key) | 90 | Ok(key) |
| 88 | } | 91 | } |
| 92 | |||
| 93 | async fn get_user_details( | ||
| 94 | public_key: &XOnlyPublicKey, | ||
| 95 | #[cfg(test)] client: &crate::client::MockConnect, | ||
| 96 | #[cfg(not(test))] client: &Client, | ||
| 97 | ) -> Result<UserRef> { | ||
| 98 | let term = console::Term::stdout(); | ||
| 99 | term.write_line("searching for your details...")?; | ||
| 100 | let user_manager = UserManager::default(); | ||
| 101 | let user_ref = user_manager | ||
| 102 | .get_user( | ||
| 103 | client, | ||
| 104 | public_key, | ||
| 105 | // 1 hour | ||
| 106 | 60 * 60, | ||
| 107 | ) | ||
| 108 | .await?; | ||
| 109 | term.clear_last_lines(1)?; | ||
| 110 | if user_ref.metadata.created_at.eq(&0) { | ||
| 111 | println!("cannot find your account metadata (name, etc) on relays",); | ||
| 112 | // TODO use secondary fallback list of relays. | ||
| 113 | // TODO better reporting of what relays were checked and what the user | ||
| 114 | // here is a starter: | ||
| 115 | // cannot find account details on relays: | ||
| 116 | // - purplepages.xyz | ||
| 117 | // - fallbackrelay1 | ||
| 118 | // - ... | ||
| 119 | // would you like to: | ||
| 120 | // [-] proceed anyway | ||
| 121 | // - add custom fallback relays | ||
| 122 | } else if user_ref.relays.created_at.eq(&0) { | ||
| 123 | println!( | ||
| 124 | "cannot find your relay list. consider using another nostr client to create one to enhance your nostr experience." | ||
| 125 | ); | ||
| 126 | // TODO better guidance on how to do this | ||
| 127 | } | ||
| 128 | Ok(user_ref) | ||
| 129 | } | ||