diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2024-11-21 16:53:17 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2024-11-21 16:53:17 +0000 |
| commit | f79014235e85554e3661b3f2a02b8fa88bc192ff (patch) | |
| tree | fceec3ff2df212148a3420af7cef81a3f818463e /src/lib/login/existing.rs | |
| parent | 91b0eac4daf92b7b740267ef203a1a8ba591974b (diff) | |
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
Diffstat (limited to 'src/lib/login/existing.rs')
| -rw-r--r-- | src/lib/login/existing.rs | 212 |
1 files changed, 212 insertions, 0 deletions
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 @@ | |||
| 1 | use std::{str::FromStr, sync::Arc, time::Duration}; | ||
| 2 | |||
| 3 | use anyhow::{bail, Context, Result}; | ||
| 4 | use nostr::nips::nip46::NostrConnectURI; | ||
| 5 | use nostr_connect::client::NostrConnect; | ||
| 6 | use nostr_sdk::{NostrSigner, PublicKey}; | ||
| 7 | |||
| 8 | use super::{ | ||
| 9 | key_encryption::decrypt_key, | ||
| 10 | print_logged_in_as, | ||
| 11 | user::{get_user_details, UserRef}, | ||
| 12 | SignerInfo, SignerInfoSource, | ||
| 13 | }; | ||
| 14 | #[cfg(not(test))] | ||
| 15 | use crate::client::Client; | ||
| 16 | #[cfg(test)] | ||
| 17 | use crate::client::MockConnect; | ||
| 18 | use crate::{ | ||
| 19 | cli_interactor::{Interactor, InteractorPrompt, PromptPasswordParms}, | ||
| 20 | client::fetch_public_key, | ||
| 21 | git::{get_git_config_item, Repo, RepoActions}, | ||
| 22 | }; | ||
| 23 | |||
| 24 | /// load signer from git config and UserProfile from cache or relays | ||
| 25 | /// | ||
| 26 | /// # Parameters | ||
| 27 | /// - `client`: include client to fetch profiles from relays that are missing | ||
| 28 | /// from cache | ||
| 29 | /// - `silent`: do not print outcome in termianl | ||
| 30 | pub async fn load_existing_login( | ||
| 31 | git_repo: &Option<&Repo>, | ||
| 32 | signer_info: &Option<SignerInfo>, | ||
| 33 | password: &Option<String>, | ||
| 34 | source: &Option<SignerInfoSource>, | ||
| 35 | #[cfg(test)] client: Option<&MockConnect>, | ||
| 36 | #[cfg(not(test))] client: Option<&Client>, | ||
| 37 | silent: bool, | ||
| 38 | prompt_for_password: bool, | ||
| 39 | ) -> Result<(Arc<dyn NostrSigner>, UserRef, SignerInfoSource)> { | ||
| 40 | let (signer_info, source) = get_signer_info(git_repo, signer_info, password, source)?; | ||
| 41 | |||
| 42 | let (signer, public_key) = get_signer(&signer_info, prompt_for_password).await?; | ||
| 43 | |||
| 44 | let user_ref = get_user_details( | ||
| 45 | &public_key, | ||
| 46 | client, | ||
| 47 | if let Some(git_repo) = git_repo { | ||
| 48 | Some(git_repo.get_path()?) | ||
| 49 | } else { | ||
| 50 | None | ||
| 51 | }, | ||
| 52 | silent, | ||
| 53 | ) | ||
| 54 | .await?; | ||
| 55 | |||
| 56 | if !silent { | ||
| 57 | print_logged_in_as(&user_ref, client.is_none(), &source)?; | ||
| 58 | } | ||
| 59 | Ok((signer, user_ref, source)) | ||
| 60 | } | ||
| 61 | |||
| 62 | /// priority order: cli arguments, local git config, global git config | ||
| 63 | fn get_signer_info( | ||
| 64 | git_repo: &Option<&Repo>, | ||
| 65 | signer_info: &Option<SignerInfo>, | ||
| 66 | password: &Option<String>, | ||
| 67 | source: &Option<SignerInfoSource>, | ||
| 68 | ) -> Result<(SignerInfo, SignerInfoSource)> { | ||
| 69 | Ok(match source { | ||
| 70 | None => { | ||
| 71 | let mut result = None; | ||
| 72 | for source in &[ | ||
| 73 | SignerInfoSource::CommandLineArguments, | ||
| 74 | SignerInfoSource::GitLocal, | ||
| 75 | SignerInfoSource::GitGlobal, | ||
| 76 | ] { | ||
| 77 | if let Ok(res) = | ||
| 78 | get_signer_info(git_repo, signer_info, password, &Some(source.clone())) | ||
| 79 | { | ||
| 80 | result = Some(res); | ||
| 81 | break; | ||
| 82 | } | ||
| 83 | } | ||
| 84 | result.context("cannot get or find signer info in cli arguments, local git config or global git config")? | ||
| 85 | } | ||
| 86 | Some(SignerInfoSource::CommandLineArguments) => { | ||
| 87 | if let Some(signer_info) = signer_info { | ||
| 88 | (signer_info.clone(), SignerInfoSource::CommandLineArguments) | ||
| 89 | } else { | ||
| 90 | bail!("cannot get signer from cli signer arguments because none were specified") | ||
| 91 | } | ||
| 92 | } | ||
| 93 | Some(SignerInfoSource::GitLocal) => { | ||
| 94 | let git_repo = | ||
| 95 | git_repo.context("failed to get local git config as no git_repo supplied")?; | ||
| 96 | if let Ok(nsec) = get_git_config_item(&Some(git_repo), "nostr.nsec") | ||
| 97 | .context("failed get local git config")? | ||
| 98 | .context("git local config item nostr.nsec doesn't exist") | ||
| 99 | { | ||
| 100 | ( | ||
| 101 | SignerInfo::Nsec { | ||
| 102 | nsec: nsec.to_string(), | ||
| 103 | password: password.clone(), | ||
| 104 | npub: get_git_config_item(&Some(git_repo), "nostr.npub") | ||
| 105 | .context("failed get local git config")?, | ||
| 106 | }, | ||
| 107 | SignerInfoSource::GitLocal, | ||
| 108 | ) | ||
| 109 | } else if let Ok(bunker_uri) = get_git_config_item(&Some(git_repo), "nostr.bunker-uri") | ||
| 110 | .context("failed get local git config")? | ||
| 111 | .context("git local config item nostr.bunker-uri doesn't exist") | ||
| 112 | { | ||
| 113 | (SignerInfo::Bunker { | ||
| 114 | bunker_uri, bunker_app_key: get_git_config_item(&Some(git_repo), "nostr.bunker-app-key") | ||
| 115 | .context("failed get local git config")? | ||
| 116 | .context("git local config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?, | ||
| 117 | npub: get_git_config_item(&Some(git_repo), "nostr.npub") | ||
| 118 | .context("failed get local git config")?, | ||
| 119 | }, SignerInfoSource::GitLocal) | ||
| 120 | } else { | ||
| 121 | bail!("no signer info in local git config") | ||
| 122 | } | ||
| 123 | } | ||
| 124 | Some(SignerInfoSource::GitGlobal) => { | ||
| 125 | if let Some(nsec) = get_git_config_item(&None, "nostr.nsec") | ||
| 126 | .context("failed to get global git config")? | ||
| 127 | { | ||
| 128 | ( | ||
| 129 | SignerInfo::Nsec { | ||
| 130 | nsec: nsec.to_string(), | ||
| 131 | password: password.clone(), | ||
| 132 | npub: get_git_config_item(&None, "nostr.npub") | ||
| 133 | .context("failed to get global git config")?, | ||
| 134 | }, | ||
| 135 | SignerInfoSource::GitGlobal, | ||
| 136 | ) | ||
| 137 | } else if let Some(bunker_uri) = get_git_config_item(&None, "nostr.bunker-uri") | ||
| 138 | .context("failed to get global git config")? | ||
| 139 | { | ||
| 140 | (SignerInfo::Bunker { | ||
| 141 | bunker_uri, bunker_app_key: get_git_config_item(&None, "nostr.bunker-app-key") | ||
| 142 | .context("failed get local git config")? | ||
| 143 | .context("git global config item nostr.bunker-uri exists but nostr.bunker-app-key doesn't")?, | ||
| 144 | npub: get_git_config_item(&None, "nostr.npub") | ||
| 145 | .context("failed get global git config")?, | ||
| 146 | }, SignerInfoSource::GitGlobal) | ||
| 147 | } else { | ||
| 148 | bail!("no signer info in global git config") | ||
| 149 | } | ||
| 150 | } | ||
| 151 | }) | ||
| 152 | } | ||
| 153 | |||
| 154 | async fn get_signer( | ||
| 155 | signer_info: &SignerInfo, | ||
| 156 | prompt_for_ncryptsec_password: bool, | ||
| 157 | ) -> Result<(Arc<dyn NostrSigner>, PublicKey)> { | ||
| 158 | match signer_info { | ||
| 159 | SignerInfo::Nsec { | ||
| 160 | nsec, | ||
| 161 | password, | ||
| 162 | npub: _, | ||
| 163 | } => { | ||
| 164 | let keys = if nsec.contains("ncryptsec") { | ||
| 165 | // TODO get user details from npub | ||
| 166 | // TODO add retry loop | ||
| 167 | // TODO in retry loop give option to login again | ||
| 168 | let password = if let Some(password) = password { | ||
| 169 | password.clone() | ||
| 170 | } else { | ||
| 171 | if !prompt_for_ncryptsec_password { | ||
| 172 | bail!("cannot login without prompts a nsec is encrypted with a password"); | ||
| 173 | } | ||
| 174 | Interactor::default() | ||
| 175 | .password(PromptPasswordParms::default().with_prompt("password")) | ||
| 176 | .context("failed to get password input from interactor.password")? | ||
| 177 | }; | ||
| 178 | decrypt_key(nsec, password.clone().as_str()) | ||
| 179 | .context("failed to decrypt key with provided password") | ||
| 180 | .context("failed to decrypt ncryptsec supplied as nsec with password")? | ||
| 181 | } else { | ||
| 182 | nostr::Keys::from_str(nsec).context("invalid nsec parameter")? | ||
| 183 | }; | ||
| 184 | let public_key = keys.public_key(); | ||
| 185 | Ok((Arc::new(keys), public_key)) | ||
| 186 | } | ||
| 187 | SignerInfo::Bunker { | ||
| 188 | bunker_uri, | ||
| 189 | bunker_app_key, | ||
| 190 | npub, | ||
| 191 | } => { | ||
| 192 | let term = console::Term::stderr(); | ||
| 193 | term.write_line("connecting to remote signer...")?; | ||
| 194 | let uri = NostrConnectURI::parse(bunker_uri)?; | ||
| 195 | let signer: Arc<dyn NostrSigner> = Arc::new(NostrConnect::new( | ||
| 196 | uri, | ||
| 197 | nostr::Keys::from_str(bunker_app_key).context("invalid app key")?, | ||
| 198 | Duration::from_secs(10 * 60), | ||
| 199 | None, | ||
| 200 | )?); | ||
| 201 | term.clear_last_lines(1)?; | ||
| 202 | let public_key = if let Some(pubic_key) = | ||
| 203 | npub.clone().and_then(|npub| PublicKey::parse(npub).ok()) | ||
| 204 | { | ||
| 205 | pubic_key | ||
| 206 | } else { | ||
| 207 | fetch_public_key(&signer).await? | ||
| 208 | }; | ||
| 209 | Ok((signer, public_key)) | ||
| 210 | } | ||
| 211 | } | ||
| 212 | } | ||