upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/lib/login/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/login/mod.rs')
-rw-r--r--src/lib/login/mod.rs233
1 files changed, 189 insertions, 44 deletions
diff --git a/src/lib/login/mod.rs b/src/lib/login/mod.rs
index b6e7623..825ec30 100644
--- a/src/lib/login/mod.rs
+++ b/src/lib/login/mod.rs
@@ -1,14 +1,19 @@
1use std::{collections::HashSet, path::Path, str::FromStr, time::Duration}; 1use std::{collections::HashSet, path::Path, str::FromStr, sync::Arc, time::Duration};
2 2
3use anyhow::{bail, Context, Result}; 3use anyhow::{bail, Context, Result};
4use console::Style;
5use dialoguer::theme::{ColorfulTheme, Theme};
4use nostr::{ 6use nostr::{
5 nips::{nip05, nip46::NostrConnectURI}, 7 nips::{nip05, nip46::NostrConnectURI},
6 PublicKey, 8 PublicKey,
7}; 9};
8use nostr_sdk::{ 10use nostr_sdk::{
9 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32, 11 Alphabet, FromBech32, JsonUtil, Keys, Kind, NostrSigner, SingleLetterTag, Timestamp, ToBech32,
12 Url,
10}; 13};
11use nostr_signer::Nip46Signer; 14use nostr_signer::Nip46Signer;
15use qrcode::QrCode;
16use tokio::sync::{oneshot, Mutex};
12 17
13#[cfg(not(test))] 18#[cfg(not(test))]
14use crate::client::Client; 19use crate::client::Client;
@@ -16,7 +21,8 @@ use crate::client::Client;
16use crate::client::MockConnect; 21use crate::client::MockConnect;
17use crate::{ 22use crate::{
18 cli_interactor::{ 23 cli_interactor::{
19 Interactor, InteractorPrompt, PromptConfirmParms, PromptInputParms, PromptPasswordParms, 24 Interactor, InteractorPrompt, Printer, PromptConfirmParms, PromptInputParms,
25 PromptPasswordParms,
20 }, 26 },
21 client::{fetch_public_key, get_event_from_global_cache, Connect}, 27 client::{fetch_public_key, get_event_from_global_cache, Connect},
22 git::{Repo, RepoActions}, 28 git::{Repo, RepoActions},
@@ -356,60 +362,198 @@ async fn fresh_login(
356 #[cfg(not(test))] client: Option<&Client>, 362 #[cfg(not(test))] client: Option<&Client>,
357 always_save: bool, 363 always_save: bool,
358) -> Result<(NostrSigner, UserRef)> { 364) -> Result<(NostrSigner, UserRef)> {
359 let mut public_key: Option<PublicKey> = None; 365 let app_key = Keys::generate();
360 // prompt for nsec 366 let app_key_secret = app_key.secret_key()?.to_secret_hex();
361 let mut prompt = "login with nostr address / nsec"; 367 let relays = if let Some(client) = client {
362 let signer = loop { 368 client
363 let input = Interactor::default() 369 .get_fallback_signer_relays()
364 .input(PromptInputParms::default().with_prompt(prompt)) 370 .iter()
365 .context("failed to get nsec input from interactor")?; 371 .flat_map(|s| Url::parse(s))
366 if let Ok(keys) = nostr::Keys::from_str(&input) { 372 .collect::<Vec<Url>>()
367 if let Err(error) = save_keys(git_repo, &keys, always_save) { 373 } else {
368 eprintln!("{error}"); 374 vec![]
375 };
376 let offline = client.is_none();
377 let nostr_connect_url = NostrConnectURI::client(app_key.public_key(), relays.clone(), "ngit");
378 let qr = generate_qr(&nostr_connect_url.to_string())?;
379
380 let printer = Arc::new(Mutex::new(Printer::default()));
381 if !offline {
382 let printer_clone = Arc::clone(&printer);
383 let mut printer_locked = printer_clone.lock().await;
384 printer_locked.printlns(qr);
385 printer_locked.println(format!(
386 "scan QR or paste into remote signer: {nostr_connect_url}"
387 ));
388 printer_locked.println_with_custom_formatting(
389 {
390 let mut s = String::new();
391 let _ = ColorfulTheme::default().format_confirm_prompt(
392 &mut s,
393 "login with nsec / bunker url / nostr address instead",
394 Some(true),
395 );
396 s
397 },
398 "? login with nsec / bunker url / nostr address instead? (y/n) › yes".to_string(),
399 );
400 }
401
402 let (tx, rx) = oneshot::channel();
403 let printer_clone = Arc::clone(&printer);
404
405 let qr_listener = tokio::spawn(async move {
406 if offline {
407 return;
408 }
409 if let Ok(nip46_signer) = Nip46Signer::new(
410 nostr_connect_url.clone(),
411 app_key.clone(),
412 Duration::from_secs(10 * 60),
413 None,
414 )
415 .await
416 {
417 let signer = NostrSigner::nip46(nip46_signer);
418 if let Ok(pub_key) = fetch_public_key(&signer).await {
419 let mut printer_locked = printer_clone.lock().await;
420 printer_locked.clear_all();
421
422 printer_locked.println_with_custom_formatting(
423 format!(
424 "{}",
425 Style::new().bold().apply_to("connected to remote signer"),
426 ),
427 "connected to remote signer".to_string(),
428 );
429 printer_locked.println("press any key to continue...".to_string());
430 let _ = tx.send(Some((signer, pub_key)));
369 } 431 }
370 break NostrSigner::Keys(keys);
371 } 432 }
372 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) { 433 });
373 uri 434 if !offline {
374 } else if input.contains('@') { 435 let _ = console::Term::stderr().read_char();
375 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await { 436 }
376 uri 437 qr_listener.abort();
377 } else { 438 let printer_clone = Arc::clone(&printer);
378 prompt = "failed. try again with nostr address / bunker uri / nsec"; 439 let mut printer = printer_clone.lock().await;
379 continue; 440 printer.clear_all();
441
442 let (signer, public_key) = {
443 if let Ok(Some((signer, public_key))) = rx.await {
444 let bunker_url = NostrConnectURI::Bunker {
445 signer_public_key: public_key,
446 relays: relays.clone(),
447 secret: None,
448 };
449 if let Err(error) = save_bunker(
450 git_repo,
451 &public_key,
452 &bunker_url.to_string(),
453 &app_key_secret,
454 always_save,
455 ) {
456 eprintln!("{error}");
380 } 457 }
458 (signer, public_key)
381 } else { 459 } else {
382 prompt = "invalid. try again with nostr address / bunker uri / nsec"; 460 let mut public_key: Option<PublicKey> = None;
383 continue; 461 // prompt for nsec
384 }; 462 let mut prompt = "login with nsec / bunker url / nostr address";
385 let app_key = Keys::generate().secret_key()?.to_secret_hex(); 463 let signer = loop {
386 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key).await { 464 let input = Interactor::default()
387 Ok(signer) => { 465 .input(PromptInputParms::default().with_prompt(prompt))
388 let pub_key = fetch_public_key(&signer).await?; 466 .context("failed to get nsec input from interactor")?;
389 if let Err(error) = 467 if let Ok(keys) = nostr::Keys::from_str(&input) {
390 save_bunker(git_repo, &pub_key, &uri.to_string(), &app_key, always_save) 468 if let Err(error) = save_keys(git_repo, &keys, always_save) {
391 { 469 eprintln!("{error}");
392 eprintln!("{error}"); 470 }
471 break NostrSigner::Keys(keys);
393 } 472 }
394 public_key = Some(pub_key); 473 let uri = if let Ok(uri) = NostrConnectURI::parse(&input) {
395 break signer; 474 uri
396 } 475 } else if input.contains('@') {
397 Err(_) => { 476 if let Ok(uri) = fetch_nip46_uri_from_nip05(&input).await {
398 prompt = "failed. try again with nostr address / bunker uri / nsec"; 477 uri
399 } 478 } else {
479 prompt = "failed. try again with nostr address / bunker uri / nsec";
480 continue;
481 }
482 } else {
483 prompt = "invalid. try again with nostr address / bunker uri / nsec";
484 continue;
485 };
486 match get_nip46_signer_from_uri_and_key(&uri.to_string(), &app_key_secret).await {
487 Ok(signer) => {
488 let pub_key = fetch_public_key(&signer).await?;
489 if let Err(error) = save_bunker(
490 git_repo,
491 &pub_key,
492 &uri.to_string(),
493 &app_key_secret,
494 always_save,
495 ) {
496 eprintln!("{error}");
497 }
498 public_key = Some(pub_key);
499 break signer;
500 }
501 Err(_) => {
502 prompt = "failed. try again with nostr address / bunker uri / nsec";
503 }
504 }
505 };
506 let public_key = if let Some(public_key) = public_key {
507 public_key
508 } else {
509 signer.public_key().await?
510 };
511 (signer, public_key)
400 } 512 }
401 }; 513 };
402 let public_key = if let Some(public_key) = public_key {
403 public_key
404 } else {
405 signer.public_key().await?
406 };
407 // lookup profile 514 // lookup profile
408 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?; 515 let user_ref = get_user_details(&public_key, client, git_repo.get_path()?, false).await?;
409 print_logged_in_as(&user_ref, client.is_none())?; 516 print_logged_in_as(&user_ref, client.is_none())?;
410 Ok((signer, user_ref)) 517 Ok((signer, user_ref))
411} 518}
412 519
520fn generate_qr(data: &str) -> Result<Vec<String>> {
521 let mut lines = vec![];
522 let qr =
523 QrCode::new(data.as_bytes()).context("failed to create QR of nostrconnect login url")?;
524 let colors = qr.to_colors();
525 let rows: Vec<&[qrcode::Color]> = colors.chunks(qr.width()).collect();
526 for (row, data) in rows.iter().enumerate() {
527 let odd = row % 2 != 0;
528 if odd {
529 continue;
530 }
531 let mut line = String::new();
532 for (col, color) in data.iter().enumerate() {
533 let top = color;
534 let mut bottom = qrcode::Color::Light;
535 if let Some(next_row_data) = rows.get(row + 1) {
536 if let Some(color) = next_row_data.get(col) {
537 bottom = *color;
538 }
539 }
540 line.push(if *top == qrcode::Color::Dark {
541 if bottom == qrcode::Color::Dark {
542 '█'
543 } else {
544 '▀'
545 }
546 } else if bottom == qrcode::Color::Dark {
547 '▄'
548 } else {
549 ' '
550 });
551 }
552 lines.push(line);
553 }
554 Ok(lines)
555}
556
413pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> { 557pub async fn fetch_nip46_uri_from_nip05(nip05: &str) -> Result<NostrConnectURI> {
414 let term = console::Term::stderr(); 558 let term = console::Term::stderr();
415 term.write_line("contacting login service provider...")?; 559 term.write_line("contacting login service provider...")?;
@@ -447,7 +591,7 @@ fn save_bunker(
447 { 591 {
448 let global = !Interactor::default().confirm( 592 let global = !Interactor::default().confirm(
449 PromptConfirmParms::default() 593 PromptConfirmParms::default()
450 .with_prompt("just for this repository?") 594 .with_prompt("save login just for this repository?")
451 .with_default(false), 595 .with_default(false),
452 )?; 596 )?;
453 let npub = public_key.to_bech32()?; 597 let npub = public_key.to_bech32()?;
@@ -523,6 +667,7 @@ fn save_keys(git_repo: &Repo, keys: &nostr::Keys, always_save: bool) -> Result<(
523 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?; 667 save_to_git_config(git_repo, &npub, &Some(nsec_string.clone()), &None, false)?;
524 } 668 }
525 } else { 669 } else {
670 eprintln!("{error}");
526 Err(error)?; 671 Err(error)?;
527 } 672 }
528 }; 673 };