chore: Improve Login Process (#70)

* wip

* simplify nostr connect uri logic

* improve wait for connection

* improve handle password

* .

* add countdown
This commit is contained in:
reya
2025-06-28 10:09:31 +07:00
committed by GitHub
parent 3c2eaabab2
commit 14076054c0
7 changed files with 337 additions and 212 deletions

1
Cargo.lock generated
View File

@@ -1122,6 +1122,7 @@ dependencies = [
"global", "global",
"gpui", "gpui",
"itertools 0.13.0", "itertools 0.13.0",
"log",
"nostr", "nostr",
"nostr-connect", "nostr-connect",
"nostr-sdk", "nostr-sdk",

View File

@@ -56,11 +56,11 @@ impl ClientKeys {
// Update keys // Update keys
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let Ok(secret_key) = SecretKey::from_slice(&secret) else { let Ok(secret_key) = SecretKey::from_slice(&secret) else {
this.set_keys(None, false, cx); this.set_keys(None, false, true, cx);
return; return;
}; };
let keys = Keys::new(secret_key); let keys = Keys::new(secret_key);
this.set_keys(Some(keys), false, cx); this.set_keys(Some(keys), false, true, cx);
}) })
.ok(); .ok();
} else if shared_state().first_run() { } else if shared_state().first_run() {
@@ -71,7 +71,7 @@ impl ClientKeys {
.ok(); .ok();
} else { } else {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.set_keys(None, false, cx); this.set_keys(None, false, true, cx);
}) })
.ok(); .ok();
} }
@@ -79,14 +79,18 @@ impl ClientKeys {
.detach(); .detach();
} }
pub(crate) fn set_keys(&mut self, keys: Option<Keys>, persist: bool, cx: &mut Context<Self>) { pub(crate) fn set_keys(
if let Some(keys) = keys.clone() { &mut self,
if persist { keys: Option<Keys>,
let write_keys = cx.write_credentials( persist: bool,
KEYRING_URL, notify: bool,
keys.public_key().to_hex().as_str(), cx: &mut Context<Self>,
keys.secret_key().as_secret_bytes(), ) {
); if persist {
if let Some(keys) = keys.as_ref() {
let username = keys.public_key().to_hex();
let password = keys.secret_key().secret_bytes();
let write_keys = cx.write_credentials(KEYRING_URL, &username, &password);
cx.background_spawn(async move { cx.background_spawn(async move {
if let Err(e) = write_keys.await { if let Err(e) = write_keys.await {
@@ -94,18 +98,22 @@ impl ClientKeys {
} }
}) })
.detach(); .detach();
cx.notify();
} }
} }
self.keys = keys; self.keys = keys;
// Make sure notify the UI after keys changes // Notify GPUI to reload UI
cx.notify(); if notify {
cx.notify();
}
} }
pub fn new_keys(&mut self, cx: &mut Context<Self>) { pub fn new_keys(&mut self, cx: &mut Context<Self>) {
self.set_keys(Some(Keys::generate()), true, cx); self.set_keys(Some(Keys::generate()), true, true, cx);
}
pub fn force_new_keys(&mut self, cx: &mut Context<Self>) {
self.set_keys(Some(Keys::generate()), true, false, cx);
} }
pub fn keys(&self) -> Keys { pub fn keys(&self) -> Keys {

View File

@@ -18,6 +18,7 @@ smallvec.workspace = true
smol.workspace = true smol.workspace = true
futures.workspace = true futures.workspace = true
reqwest.workspace = true reqwest.workspace = true
log.workspace = true
webbrowser = "1.0.4" webbrowser = "1.0.4"
qrcode-generator = "5.0.0" qrcode-generator = "5.0.0"

View File

@@ -6,6 +6,7 @@ pub struct CoopAuthUrlHandler;
impl AuthUrlHandler for CoopAuthUrlHandler { impl AuthUrlHandler for CoopAuthUrlHandler {
fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<()>> { fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<()>> {
Box::pin(async move { Box::pin(async move {
log::info!("Received Auth URL: {auth_url}");
webbrowser::open(auth_url.as_str())?; webbrowser::open(auth_url.as_str())?;
Ok(()) Ok(())
}) })

View File

@@ -22,6 +22,8 @@ use ui::notification::Notification;
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::{ContextModal, Disableable, Sizable, StyledExt}; use ui::{ContextModal, Disableable, Sizable, StyledExt};
const TIMEOUT: u64 = 30;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
Login::new(window, cx) Login::new(window, cx)
} }
@@ -31,16 +33,15 @@ pub struct Login {
relay_input: Entity<InputState>, relay_input: Entity<InputState>,
connection_string: Entity<NostrConnectURI>, connection_string: Entity<NostrConnectURI>,
qr_image: Entity<Option<Arc<Image>>>, qr_image: Entity<Option<Arc<Image>>>,
// Signer that created by Connection String
active_signer: Entity<Option<NostrConnect>>,
// Error for the key input // Error for the key input
error: Entity<Option<SharedString>>, error: Entity<Option<SharedString>>,
is_logging_in: bool, countdown: Entity<Option<u64>>,
logging_in: bool,
// Panel // Panel
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
#[allow(unused)] #[allow(unused)]
subscriptions: SmallVec<[Subscription; 5]>, subscriptions: SmallVec<[Subscription; 3]>,
} }
impl Login { impl Login {
@@ -66,15 +67,9 @@ impl Login {
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME) NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
}); });
// Convert the Connection String into QR Image
let qr_image = cx.new(|_| None); let qr_image = cx.new(|_| None);
let async_qr_image = qr_image.downgrade();
// Keep track of the signer that created by Connection String
let active_signer = cx.new(|_| None);
let async_active_signer = active_signer.downgrade();
let error = cx.new(|_| None); let error = cx.new(|_| None);
let countdown = cx.new(|_| None);
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
// Subscribe to key input events and process login when the user presses enter // Subscribe to key input events and process login when the user presses enter
@@ -95,36 +90,11 @@ impl Login {
}), }),
); );
// Observe the Connect URI that changes when the relay is changed // Observe changes to the Nostr Connect URI and wait for a connection
subscriptions.push(cx.observe_new::<NostrConnectURI>(move |uri, _window, cx| {
let client_keys = ClientKeys::get_global(cx).keys();
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
if let Ok(mut signer) = NostrConnect::new(uri.to_owned(), client_keys, timeout, None) {
// Automatically open auth url
signer.auth_url_handler(CoopAuthUrlHandler);
async_active_signer
.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
})
.ok();
}
// Update the QR Image with the new connection string
async_qr_image
.update(cx, |this, cx| {
*this = string_to_qr(&uri.to_string());
cx.notify();
})
.ok();
}));
subscriptions.push(cx.observe_in( subscriptions.push(cx.observe_in(
&connection_string, &connection_string,
window, window,
|this, entity, _window, cx| { |this, entity, window, cx| {
let connection_string = entity.read(cx).clone(); let connection_string = entity.read(cx).clone();
let client_keys = ClientKeys::get_global(cx).keys(); let client_keys = ClientKeys::get_global(cx).keys();
@@ -143,65 +113,86 @@ impl Login {
Ok(mut signer) => { Ok(mut signer) => {
// Automatically open auth url // Automatically open auth url
signer.auth_url_handler(CoopAuthUrlHandler); signer.auth_url_handler(CoopAuthUrlHandler);
// Wait for connection in the background
this.active_signer.update(cx, |this, cx| { this.wait_for_connection(signer, window, cx);
*this = Some(signer);
cx.notify();
});
} }
Err(_) => { Err(e) => {
log::error!("Failed to create Nostr Connect") window.push_notification(
Notification::error(e.to_string()).title("Nostr Connect"),
cx,
);
} }
} }
}, },
)); ));
subscriptions.push( // Create a Nostr Connect URI and QR Code 800ms after opening the login screen
cx.observe_in(&active_signer, window, |this, entity, window, cx| { cx.spawn_in(window, async move |this, cx| {
if let Some(signer) = entity.read(cx).as_ref() { cx.background_executor()
// Wait for connection from remote signer .timer(Duration::from_millis(800))
this.wait_for_connection(signer.to_owned(), window, cx); .await;
} this.update(cx, |this, cx| {
}), this.connection_string.update(cx, |_, cx| {
); cx.notify();
})
})
.ok();
})
.detach();
Self { Self {
name: "Login".into(), name: "Login".into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
is_logging_in: false, logging_in: false,
countdown,
key_input, key_input,
relay_input, relay_input,
connection_string, connection_string,
qr_image, qr_image,
error, error,
active_signer,
subscriptions, subscriptions,
} }
} }
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.is_logging_in { if self.logging_in {
return; return;
}; };
// Prevent duplicate login requests // Prevent duplicate login requests
self.set_logging_in(true, cx); self.set_logging_in(true, cx);
// Disable the input
self.key_input.update(cx, |this, cx| {
this.set_loading(true, cx);
this.set_disabled(true, cx);
});
// Content can be secret key or bunker:// // Content can be secret key or bunker://
match self.key_input.read(cx).value().to_string() { match self.key_input.read(cx).value().to_string() {
s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx), s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx),
s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx), s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx),
s if s.starts_with("bunker://") => self.login_with_bunker(&s, window, cx), s if s.starts_with("bunker://") => self.login_with_bunker(s, window, cx),
_ => self.set_error("You must provide a valid Private Key or Bunker.", cx), _ => self.set_error(
"You must provide a valid Private Key or Bunker.",
window,
cx,
),
}; };
} }
fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) { fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
let current_view = cx.entity().downgrade(); let current_view = cx.entity().downgrade();
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let weak_input = pwd_input.downgrade(); let weak_pwd_input = pwd_input.downgrade();
let confirm_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let weak_confirm_input = confirm_input.downgrade();
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
let weak_input = weak_input.clone(); let weak_pwd_input = weak_pwd_input.clone();
let weak_confirm_input = weak_confirm_input.clone();
let view_cancel = current_view.clone(); let view_cancel = current_view.clone();
let view_ok = current_view.clone(); let view_ok = current_view.clone();
@@ -211,36 +202,38 @@ impl Login {
"Password to decrypt your key *".into() "Password to decrypt your key *".into()
}; };
let description: Option<SharedString> = if content.starts_with("ncryptsec1") { let description: SharedString = if content.starts_with("ncryptsec1") {
Some("Coop will only stored the encrypted version".into()) "Coop will only store the encrypted version of your keys".into()
} else { } else {
None "Coop will use the password to encrypt your keys. \
You will need this password to decrypt your keys for future use."
.into()
}; };
this.overlay_closable(false) this.overlay_closable(false)
.show_close(false) .show_close(false)
.keyboard(false) .keyboard(false)
.confirm() .confirm()
.on_cancel(move |_, _window, cx| { .on_cancel(move |_, window, cx| {
view_cancel view_cancel
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.set_error("Password is required", cx); this.set_error("Password is required", window, cx);
}) })
.ok(); .ok();
true true
}) })
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
let value = weak_input let value = weak_pwd_input
.read_with(cx, |state, _cx| state.value().to_owned())
.ok();
let confirm = weak_confirm_input
.read_with(cx, |state, _cx| state.value().to_owned()) .read_with(cx, |state, _cx| state.value().to_owned())
.ok(); .ok();
view_ok view_ok
.update(cx, |this, cx| { .update(cx, |this, cx| {
if let Some(password) = value { this.verify_password(value, confirm, window, cx);
this.login_with_keys(password.to_string(), window, cx);
} else {
this.set_error("Password is required", cx);
}
}) })
.ok(); .ok();
true true
@@ -252,23 +245,78 @@ impl Login {
.w_full() .w_full()
.flex() .flex()
.flex_col() .flex_col()
.gap_1() .gap_2()
.text_sm() .text_sm()
.child(label) .child(
.child(TextInput::new(&pwd_input).small()) div()
.when_some(description, |this, description| { .flex()
.flex_col()
.gap_1()
.child(label)
.child(TextInput::new(&pwd_input).small()),
)
.when(content.starts_with("nsec1"), |this| {
this.child( this.child(
div() div()
.text_xs() .flex()
.italic() .flex_col()
.text_color(cx.theme().text_placeholder) .gap_1()
.child(description), .child("Confirm your password *")
.child(TextInput::new(&confirm_input).small()),
) )
}), })
.child(
div()
.text_xs()
.italic()
.text_color(cx.theme().text_placeholder)
.child(description),
),
) )
}); });
} }
fn verify_password(
&mut self,
password: Option<SharedString>,
confirm: Option<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(password) = password else {
self.set_error("Password is required", window, cx);
return;
};
if password.is_empty() {
self.set_error("Password is required", window, cx);
return;
}
// Skip verification if password starts with "ncryptsec1"
if password.starts_with("ncryptsec1") {
self.login_with_keys(password.to_string(), window, cx);
return;
}
let Some(confirm) = confirm else {
self.set_error("You must confirm your password", window, cx);
return;
};
if confirm.is_empty() {
self.set_error("You must confirm your password", window, cx);
return;
}
if password != confirm {
self.set_error("Passwords do not match", window, cx);
return;
}
self.login_with_keys(password.to_string(), window, cx);
}
fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) { fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
let value = self.key_input.read(cx).value().to_string(); let value = self.key_input.read(cx).value().to_string();
let secret_key = if value.starts_with("nsec1") { let secret_key = if value.starts_with("nsec1") {
@@ -283,59 +331,75 @@ impl Login {
if let Some(secret_key) = secret_key { if let Some(secret_key) = secret_key {
let keys = Keys::new(secret_key); let keys = Keys::new(secret_key);
Identity::global(cx).update(cx, |this, cx| { Identity::global(cx).update(cx, |this, cx| {
this.write_keys(&keys, password, cx); this.write_keys(&keys, password, cx);
this.set_signer(keys, window, cx); this.set_signer(keys, window, cx);
}); });
} else { } else {
self.set_error("Secret Key is invalid", cx); self.set_error("Secret Key is invalid", window, cx);
} }
} }
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) { fn login_with_bunker(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
let Ok(uri) = NostrConnectURI::parse(content) else { let Ok(uri) = NostrConnectURI::parse(content) else {
self.set_error("Bunker URL is not valid", cx); self.set_error("Bunker URL is not valid", window, cx);
return; return;
}; };
let client_keys = ClientKeys::get_global(cx).keys(); let client_keys = ClientKeys::get_global(cx).keys();
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2); let timeout = Duration::from_secs(TIMEOUT);
// .unwrap() is fine here because there's no error handling for bunker uri
let Ok(mut signer) = NostrConnect::new(uri, client_keys, timeout, None) else { let mut signer = NostrConnect::new(uri, client_keys, timeout, None).unwrap();
self.set_error("Failed to create remote signer", cx); // Handle auth url with the default browser
return;
};
// Automatically open auth url
signer.auth_url_handler(CoopAuthUrlHandler); signer.auth_url_handler(CoopAuthUrlHandler);
let (tx, rx) = oneshot::channel::<Option<(NostrConnect, NostrConnectURI)>>(); // Start countdown
cx.spawn_in(window, async move |this, cx| {
// Verify remote signer connection for i in (0..=TIMEOUT).rev() {
cx.background_spawn(async move { if i == 0 {
if let Ok(bunker_uri) = signer.bunker_uri().await { this.update(cx, |this, cx| {
tx.send(Some((signer, bunker_uri))).ok(); this.set_countdown(None, cx);
} else { })
tx.send(None).ok(); .ok();
} else {
this.update(cx, |this, cx| {
this.set_countdown(Some(i), cx);
})
.ok();
}
cx.background_executor().timer(Duration::from_secs(1)).await;
} }
}) })
.detach(); .detach();
// Handle connection
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Ok(Some((signer, uri))) = rx.await { match signer.bunker_uri().await {
cx.update(|window, cx| { Ok(bunker_uri) => {
Identity::global(cx).update(cx, |this, cx| { cx.update(|window, cx| {
this.write_bunker(&uri, cx); window.push_notification("Logging in...", cx);
this.set_signer(signer, window, cx); Identity::global(cx).update(cx, |this, cx| {
}); this.write_bunker(&bunker_uri, cx);
}) this.set_signer(signer, window, cx);
.ok(); });
} else { })
this.update(cx, |this, cx| { .ok();
let msg = "Connection to the Remote Signer failed or timed out"; }
this.set_error(msg, cx); Err(error) => {
}) cx.update(|window, cx| {
.ok(); this.update(cx, |this, cx| {
// Force reset the client keys without notify UI
ClientKeys::global(cx).update(cx, |this, cx| {
log::info!("Timeout occurred. Reset client keys");
this.force_new_keys(cx);
});
this.set_error(error.to_string(), window, cx);
})
.ok();
})
.ok();
}
} }
}) })
.detach(); .detach();
@@ -347,40 +411,30 @@ impl Login {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let (tx, rx) = oneshot::channel::<Option<(NostrConnectURI, NostrConnect)>>();
cx.background_spawn(async move {
match signer.bunker_uri().await {
Ok(bunker_uri) => {
tx.send(Some((bunker_uri, signer))).ok();
}
Err(e) => {
log::error!("Nostr Connect (Client): {e}");
tx.send(None).ok();
}
}
})
.detach();
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Ok(Some((uri, signer))) = rx.await { match signer.bunker_uri().await {
cx.update(|window, cx| { Ok(uri) => {
Identity::global(cx).update(cx, |this, cx| { cx.update(|window, cx| {
this.write_bunker(&uri, cx); Identity::global(cx).update(cx, |this, cx| {
this.set_signer(signer, window, cx); this.write_bunker(&uri, cx);
}); this.set_signer(signer, window, cx);
}) });
.ok();
} else {
cx.update(|window, cx| {
// Refresh the active signer
this.update(cx, |this, cx| {
window.push_notification(Notification::error("Connection failed"), cx);
this.change_relay(window, cx);
}) })
.ok(); .ok();
}) }
.ok(); Err(e) => {
cx.update(|window, cx| {
// Only send notifications on the login screen
this.update(cx, |_, cx| {
window.push_notification(
Notification::error(e.to_string()).title("Nostr Connect"),
cx,
);
})
.ok();
})
.ok();
}
} }
}) })
.detach(); .detach();
@@ -402,13 +456,31 @@ impl Login {
}); });
} }
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) { fn set_error(
&mut self,
message: impl Into<SharedString>,
window: &mut Window,
cx: &mut Context<Self>,
) {
// Reset the log in state
self.set_logging_in(false, cx); self.set_logging_in(false, cx);
// Reset the countdown
self.set_countdown(None, cx);
// Update error message
self.error.update(cx, |this, cx| { self.error.update(cx, |this, cx| {
*this = Some(message.into()); *this = Some(message.into());
cx.notify(); cx.notify();
}); });
// Re enable the input
self.key_input.update(cx, |this, cx| {
this.set_value("", window, cx);
this.set_loading(false, cx);
this.set_disabled(false, cx);
});
// Clear the error message after 3 secs // Clear the error message after 3 secs
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(3)).await; cx.background_executor().timer(Duration::from_secs(3)).await;
@@ -425,9 +497,16 @@ impl Login {
} }
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) { fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
self.is_logging_in = status; self.logging_in = status;
cx.notify(); cx.notify();
} }
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
self.countdown.update(cx, |this, cx| {
*this = i;
cx.notify();
});
}
} }
impl Panel for Login { impl Panel for Login {
@@ -502,12 +581,24 @@ impl Render for Login {
Button::new("login") Button::new("login")
.label("Continue") .label("Continue")
.primary() .primary()
.loading(self.is_logging_in) .loading(self.logging_in)
.disabled(self.is_logging_in) .disabled(self.logging_in)
.on_click(cx.listener(move |this, _, window, cx| { .on_click(cx.listener(move |this, _, window, cx| {
this.login(window, cx); this.login(window, cx);
})), })),
) )
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
this.child(
div()
.text_xs()
.text_center()
.text_color(cx.theme().text_muted)
.child(SharedString::from(format!(
"Approve connection request from your signer in {} seconds",
i
))),
)
})
.when_some(self.error.read(cx).clone(), |this, error| { .when_some(self.error.read(cx).clone(), |this, error| {
this.child( this.child(
div() div()

View File

@@ -632,7 +632,7 @@ impl Globals {
} }
fn is_first_run() -> Result<bool, anyhow::Error> { fn is_first_run() -> Result<bool, anyhow::Error> {
let flag = support_dir().join(".coop_first_run"); let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));
if !flag.exists() { if !flag.exists() {
fs::write(&flag, "")?; fs::write(&flag, "")?;

View File

@@ -9,7 +9,7 @@ use global::{
}; };
use gpui::{ use gpui::{
div, prelude::FluentBuilder, red, App, AppContext, Context, Entity, Global, ParentElement, div, prelude::FluentBuilder, red, App, AppContext, Context, Entity, Global, ParentElement,
SharedString, Styled, Subscription, Task, Window, SharedString, Styled, Subscription, Task, WeakEntity, Window,
}; };
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -227,10 +227,15 @@ impl Identity {
) { ) {
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true)); let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
let weak_input = pwd_input.downgrade(); let weak_input = pwd_input.downgrade();
let error: Entity<Option<SharedString>> = cx.new(|_| None); let error: Entity<Option<SharedString>> = cx.new(|_| None);
let weak_error = error.downgrade(); let weak_error = error.downgrade();
let entity = cx.weak_entity();
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
let entity = entity.clone();
let entity_clone = entity.clone();
let weak_input = weak_input.clone(); let weak_input = weak_input.clone();
let weak_error = weak_error.clone(); let weak_error = weak_error.clone();
@@ -239,56 +244,25 @@ impl Identity {
.keyboard(false) .keyboard(false)
.confirm() .confirm()
.on_cancel(move |_, _window, cx| { .on_cancel(move |_, _window, cx| {
Identity::global(cx).update(cx, |this, cx| { entity
this.set_profile(None, cx); .update(cx, |this, cx| {
}); this.set_profile(None, cx);
})
.ok();
// Close modal
true true
}) })
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
let value = weak_input let weak_error = weak_error.clone();
.read_with(cx, |state, _cx| state.value().to_string()) let password = weak_input
.read_with(cx, |state, _cx| state.value().to_owned())
.ok(); .ok();
if let Some(password) = value { entity_clone
if password.is_empty() { .update(cx, |this, cx| {
weak_error this.verify_keys(enc, password, weak_error, window, cx);
.update(cx, |this, cx| { })
*this = Some("Password cannot be empty".into()); .ok();
cx.notify();
})
.ok();
return false;
};
Identity::global(cx).update(cx, |_, cx| {
let weak_error = weak_error.clone();
let task: Task<Option<SecretKey>> = cx.background_spawn(async move {
// Decrypt the password in the background to prevent blocking the main thread
enc.decrypt(&password).ok()
});
cx.spawn_in(window, async move |this, cx| {
if let Some(secret) = task.await {
cx.update(|window, cx| {
window.close_modal(cx);
this.update(cx, |this, cx| {
this.set_signer(Keys::new(secret), window, cx);
})
.ok();
})
.ok();
} else {
weak_error
.update(cx, |this, cx| {
*this = Some("Invalid password".into());
cx.notify();
})
.ok();
}
})
.detach();
});
}
false false
}) })
@@ -316,6 +290,55 @@ impl Identity {
}); });
} }
pub(crate) fn verify_keys(
&mut self,
enc: EncryptedSecretKey,
password: Option<SharedString>,
error: WeakEntity<Option<SharedString>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(password) = password else {
_ = error.update(cx, |this, cx| {
*this = Some("Password is required".into());
cx.notify();
});
return;
};
if password.is_empty() {
_ = error.update(cx, |this, cx| {
*this = Some("Password cannot be empty".into());
cx.notify();
});
return;
}
// Decrypt the password in the background to prevent blocking the main thread
let task: Task<Option<SecretKey>> =
cx.background_spawn(async move { enc.decrypt(&password).ok() });
cx.spawn_in(window, async move |this, cx| {
if let Some(secret) = task.await {
cx.update(|window, cx| {
window.close_modal(cx);
// Update user's signer with decrypted secret key
this.update(cx, |this, cx| {
this.set_signer(Keys::new(secret), window, cx);
})
.ok();
})
.ok();
} else {
_ = error.update(cx, |this, cx| {
*this = Some("Invalid password".into());
cx.notify();
});
}
})
.detach();
}
/// Sets a new signer for the client and updates user identity /// Sets a new signer for the client and updates user identity
pub fn set_signer<S>(&self, signer: S, window: &mut Window, cx: &mut Context<Self>) pub fn set_signer<S>(&self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where where
@@ -453,7 +476,7 @@ impl Identity {
cx.background_spawn(async move { cx.background_spawn(async move {
if let Ok(enc_key) = if let Ok(enc_key) =
EncryptedSecretKey::new(keys.secret_key(), &password, 16, KeySecurity::Medium) EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
{ {
let client = shared_state().client(); let client = shared_state().client();
let keys = Keys::generate(); let keys = Keys::generate();