From f79014235e85554e3661b3f2a02b8fa88bc192ff Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Thu, 21 Nov 2024 16:53:17 +0000 Subject: feat(login): overhaul login experience * simplify login menu, making it more accessable to newcomers and easier to select remote signer options * enable `ngit login` to work from anywhere (not just a git repo) * assume fresh login details saved to global git config but fallback to local repository * maintain local repository login via `ngit login --local` * maintain login via CLI arguments eg `ngit send --nsec nsec123` * nudge users to remember nsec when pasting in ncryptsec for a better UX, whilst maintaining the option to be prompted for password everytime * create placeholder menu items for help menu and create account --- src/lib/login/existing.rs | 212 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 src/lib/login/existing.rs (limited to 'src/lib/login/existing.rs') diff --git a/src/lib/login/existing.rs b/src/lib/login/existing.rs new file mode 100644 index 0000000..e388a34 --- /dev/null +++ b/src/lib/login/existing.rs @@ -0,0 +1,212 @@ +use std::{str::FromStr, sync::Arc, time::Duration}; + +use anyhow::{bail, Context, Result}; +use nostr::nips::nip46::NostrConnectURI; +use nostr_connect::client::NostrConnect; +use nostr_sdk::{NostrSigner, PublicKey}; + +use super::{ + key_encryption::decrypt_key, + print_logged_in_as, + user::{get_user_details, UserRef}, + SignerInfo, SignerInfoSource, +}; +#[cfg(not(test))] +use crate::client::Client; +#[cfg(test)] +use crate::client::MockConnect; +use crate::{ + cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, + client::fetch_public_key, + git::{get_git_config_item, Repo, RepoActions}, +}; + +/// load signer from git config and UserProfile from cache or relays +/// +/// # Parameters +/// - `client`: include client to fetch profiles from relays that are missing +/// from cache +/// - `silent`: do not print outcome in termianl +pub async fn load_existing_login( + git_repo: &Option<&Repo>, + signer_info: &Option, + password: &Option, + source: &Option, + #[cfg(test)] client: Option<&MockConnect>, + #[cfg(not(test))] client: Option<&Client>, + silent: bool, + prompt_for_password: bool, +) -> Result<(Arc, UserRef, SignerInfoSource)> { + let (signer_info, source) = get_signer_info(git_repo, signer_info, password, source)?; + + let (signer, public_key) = get_signer(&signer_info, prompt_for_password).await?; + + let user_ref = get_user_details( + &public_key, + client, + if let Some(git_repo) = git_repo { + Some(git_repo.get_path()?) + } else { + None + }, + silent, + ) + .await?; + + if !silent { + print_logged_in_as(&user_ref, client.is_none(), &source)?; + } + Ok((signer, user_ref, source)) +} + +/// priority order: cli arguments, local git config, global git config +fn get_signer_info( + git_repo: &Option<&Repo>, + signer_info: &Option, + password: &Option, + source: &Option, +) -> Result<(SignerInfo, SignerInfoSource)> { + Ok(match source { + None => { + let mut result = None; + for source in &[ + SignerInfoSource::CommandLineArguments, + SignerInfoSource::GitLocal, + SignerInfoSource::GitGlobal, + ] { + if let Ok(res) = + get_signer_info(git_repo, signer_info, password, &Some(source.clone())) + { + result = Some(res); + break; + } + } + result.context("cannot get or find signer info in cli arguments, local git config or global git config")? + } + Some(SignerInfoSource::CommandLineArguments) => { + if let Some(signer_info) = signer_info { + (signer_info.clone(), SignerInfoSource::CommandLineArguments) + } else { + bail!("cannot get signer from cli signer arguments because none were specified") + } + } + Some(SignerInfoSource::GitLocal) => { + let git_repo = + git_repo.context("failed to get local git config as no git_repo supplied")?; + if let Ok(nsec) = get_git_config_item(&Some(git_repo), "nostr.nsec") + .context("failed get local git config")? + .context("git local config item nostr.nsec doesn't exist") + { + ( + SignerInfo::Nsec { + nsec: nsec.to_string(), + password: password.clone(), + npub: get_git_config_item(&Some(git_repo), "nostr.npub") + .context("failed get local git config")?, + }, + SignerInfoSource::GitLocal, + ) + } else if let Ok(bunker_uri) = get_git_config_item(&Some(git_repo), "nostr.bunker-uri") + .context("failed get local git config")? + .context("git local config item nostr.bunker-uri doesn't exist") + { + (SignerInfo::Bunker { + bunker_uri, bunker_app_key: get_git_config_item(&Some(git_repo), "nostr.bunker-app-key") + .context("failed get local git config")? + .context("git local config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?, + npub: get_git_config_item(&Some(git_repo), "nostr.npub") + .context("failed get local git config")?, + }, SignerInfoSource::GitLocal) + } else { + bail!("no signer info in local git config") + } + } + Some(SignerInfoSource::GitGlobal) => { + if let Some(nsec) = get_git_config_item(&None, "nostr.nsec") + .context("failed to get global git config")? + { + ( + SignerInfo::Nsec { + nsec: nsec.to_string(), + password: password.clone(), + npub: get_git_config_item(&None, "nostr.npub") + .context("failed to get global git config")?, + }, + SignerInfoSource::GitGlobal, + ) + } else if let Some(bunker_uri) = get_git_config_item(&None, "nostr.bunker-uri") + .context("failed to get global git config")? + { + (SignerInfo::Bunker { + bunker_uri, bunker_app_key: get_git_config_item(&None, "nostr.bunker-app-key") + .context("failed get local git config")? + .context("git global config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?, + npub: get_git_config_item(&None, "nostr.npub") + .context("failed get global git config")?, + }, SignerInfoSource::GitGlobal) + } else { + bail!("no signer info in global git config") + } + } + }) +} + +async fn get_signer( + signer_info: &SignerInfo, + prompt_for_ncryptsec_password: bool, +) -> Result<(Arc, PublicKey)> { + match signer_info { + SignerInfo::Nsec { + nsec, + password, + npub: _, + } => { + let keys = if nsec.contains("ncryptsec") { + // TODO get user details from npub + // TODO add retry loop + // TODO in retry loop give option to login again + let password = if let Some(password) = password { + password.clone() + } else { + if !prompt_for_ncryptsec_password { + bail!("cannot login without prompts a nsec is encrypted with a password"); + } + Interactor::default() + .password(PromptPasswordParms::default().with_prompt("password")) + .context("failed to get password input from interactor.password")? + }; + decrypt_key(nsec, password.clone().as_str()) + .context("failed to decrypt key with provided password") + .context("failed to decrypt ncryptsec supplied as nsec with password")? + } else { + nostr::Keys::from_str(nsec).context("invalid nsec parameter")? + }; + let public_key = keys.public_key(); + Ok((Arc::new(keys), public_key)) + } + SignerInfo::Bunker { + bunker_uri, + bunker_app_key, + npub, + } => { + let term = console::Term::stderr(); + term.write_line("connecting to remote signer...")?; + let uri = NostrConnectURI::parse(bunker_uri)?; + let signer: Arc = Arc::new(NostrConnect::new( + uri, + nostr::Keys::from_str(bunker_app_key).context("invalid app key")?, + Duration::from_secs(10 * 60), + None, + )?); + term.clear_last_lines(1)?; + let public_key = if let Some(pubic_key) = + npub.clone().and_then(|npub| PublicKey::parse(npub).ok()) + { + pubic_key + } else { + fetch_public_key(&signer).await? + }; + Ok((signer, public_key)) + } + } +} -- cgit v1.2.3