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:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(())
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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, "")?;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user